Skip to content

Commit ef71b47

Browse files
committed
Add initial version
1 parent d0ae162 commit ef71b47

File tree

13 files changed

+523
-2
lines changed

13 files changed

+523
-2
lines changed

.travis.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
language: python
2+
sudo: false
3+
cache:
4+
- pip
5+
python:
6+
- '3.4'
7+
- '3.5'
8+
- '3.6'
9+
- '3.7-dev'
10+
matrix:
11+
fast_finish: true
12+
allow_failures:
13+
- python: '3.7-dev'
14+
install:
15+
- pip install -r requirements-dev.txt
16+
- pip install -U codecov
17+
script:
18+
- coverage run --source yeelib -m pytest tests
19+
after_success:
20+
- codecov
21+
deploy:
22+
provider: pypi
23+
user: codingjoe
24+
password:
25+
secure: jn763GkmO/nM7TJ9sekgBbFRMI1Z8XijY4ej1ztcJgIwWvZlAWga02sBEnjyFzeqnNmYWAnM50F0oLAvPxmhwIFEUGby2h2Ppy3RhyyNsYi3nQiFKC9/rr0oMJG3xtqyGIJx35pyMM8IzrwQ/tDAcdBRl4HgxnDTLAjgf6Wb1mOhwvQ+/+AZqVqHS0IdXM80K/SgO6LsgL1HgBHdJ7m9i/FUmP5RfDXAPIddd5n81Y73y0ylqsHFgSh8/LtAEcmlJEQIkZDp3jJYM5vxzV4h0LQ6vU7Y1p9RnC6cKEQd+d70iAr+p9apwlwRshaYGy8QVdSbt1vPzpx3mg1Jj1+JpaTmjkC6Ki3XGwcLtARc4MeARnrtB8o0R+QP3KnbfxxQ1SFmvfglZo2HXss6PBB2r6XhAqz7n3zwa7FDqkPb01sytXumnIjckD1E+68X4bR31i627wtIJ5DzTfJYbXAcLbwIPcvnwtEqtmP5IVZJW+xl7ImuyTeslDMx9lT56r2SuGeXZ/B1FRuCRa0hkWasY2DRRo+xrlfleAoMCrLTGKewnNtu9R6r8F1nPUIboLBpirqYStgD86La4uFJai8yer92zNnAue1q7gqj6hAEzoBITGxvDMiy1huWnvnVXP2R9AU/G+oFIGQgbBrXwOVDqeTHla9MGfID3j9rWSi1oNs=
26+
on:
27+
tags: true
28+
distributions: sdist bdist_wheel
29+
repo: codingjoe/yeelib

README.md

Lines changed: 0 additions & 2 deletions
This file was deleted.

README.rst

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
======
2+
yeelib
3+
======
4+
5+
**Python library for Xiaomi Mi Yeelight.**
6+
7+
``yeelib`` is an ``asyncio`` based Python library to control Yeelights.
8+
It has no external dependencies and is blazing fast!
9+
10+
11+
Getting Started
12+
---------------
13+
14+
Install
15+
16+
.. code:: shell
17+
18+
pip install yeelib
19+
20+
Example:
21+
22+
.. code:: python
23+
24+
import asyncio
25+
26+
from yeelib import search_bulbs
27+
28+
29+
@asyncio.coroutine
30+
def turn_all_lights_on(bulbs):
31+
while True:
32+
print(bulbs)
33+
for b in bulbs.values():
34+
asyncio.Task(b.send_command("set_power",
35+
["off", "sudden", 40]))
36+
yield from asyncio.sleep(10)
37+
38+
39+
def main():
40+
loop = asyncio.get_event_loop()
41+
with search_bulbs() as bulbs:
42+
loop.create_task(turn_all_lights_on(bulbs))
43+
try:
44+
loop.run_forever()
45+
except KeyboardInterrupt:
46+
loop.stop()
47+
48+
49+
if __name__ == '__main__':
50+
main()
51+
52+
The script above will turn all off every 10 seconds.
53+
The following script does the same thing, but note how you can define the bulbs
54+
prior calling the ``search_bulbs`` context manager. This works do to the fact
55+
that dictionaries are mutable in Python.
56+
57+
.. code:: python
58+
59+
import asyncio
60+
61+
from yeelib import search_bulbs
62+
63+
64+
@asyncio.coroutine
65+
def turn_all_lights_on(bulbs):
66+
while True:
67+
print(bulbs)
68+
for b in bulbs.values():
69+
asyncio.Task(b.send_command("set_power",
70+
["off", "sudden", 40]))
71+
yield from asyncio.sleep(10)
72+
73+
74+
def main():
75+
bulbs = {}
76+
asyncio.Task(turn_all_lights_on(bulbs))
77+
loop = asyncio.get_event_loop()
78+
try:
79+
with search_bulbs(bulbs):
80+
loop.run_forever()
81+
except KeyboardInterrupt:
82+
loop.stop()
83+
84+
85+
if __name__ == '__main__':
86+
main()
87+
88+
89+
Specifications
90+
--------------
91+
92+
For more information check out the Yeelight developer documentation.
93+
http://www.yeelight.com/download/Yeelight_Inter-Operation_Spec.pdf

requirements-dev.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-e .
2+
pytest
3+
coverage
4+
mocket

setup.cfg

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[metadata]
2+
name = yeelib
3+
author = Johannes Hoppe
4+
author-email = info@johanneshoppe.com
5+
summary = Pyhton library for Xioami Mi Yeelight.
6+
description-file = README.rst
7+
description-content-type = text/x-rst; charset=UTF-8
8+
home-page = https://github.com/codingjoe/yeelib
9+
license = Apache-2
10+
classifier =
11+
Development Status :: 4 - Beta
12+
Environment :: Console
13+
Intended Audience :: Developers
14+
Intended Audience :: Information Technology
15+
License :: OSI Approved :: Apache Software License
16+
Operating System :: OS Independent
17+
Programming Language :: Python
18+
keywords =
19+
light
20+
yeelight
21+
22+
[files]
23+
packages =
24+
yeelib
25+
26+
[pydocstyle]
27+
add_ignore = D1

setup.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env python
2+
from setuptools import setup
3+
4+
setup(
5+
setup_requires=['pbr'],
6+
pbr=True,
7+
)

tests/__init__.py

Whitespace-only changes.

tests/test_bulbs.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import asyncio
2+
3+
from mocket import Mocket, MocketEntry, mocketize
4+
5+
from yeelib import Bulb
6+
7+
8+
class TestBulb:
9+
bulb_addr = ('192.168.1.239', 55443)
10+
11+
@mocketize
12+
def test_send_command(self):
13+
Mocket.register(MocketEntry(self.bulb_addr, [b'{"id":1, "result":["ok"]}\r\n']))
14+
with Bulb(*self.bulb_addr) as b:
15+
loop = asyncio.get_event_loop()
16+
response = loop.run_until_complete(b.send_command('set_ct_abx', [3500, 'smooth', 500]))
17+
18+
assert response == {'id': 1, 'result': ['ok']}
19+
20+
def test_repr(self):
21+
with Bulb(*self.bulb_addr) as b:
22+
assert repr(b) == '<Bulb: 192.168.1.239>'
23+
24+
def test_kwargs(self):
25+
kwargs = {
26+
'id': '0x000000000015243f',
27+
'model': 'color',
28+
'fw_ver': 18,
29+
'support': 'get_prop set_default set_power toggle set_bright start_cf stop_cf set_scene'
30+
'cron_add cron_get cron_del set_ct_abx set_rgb',
31+
'power': 'on',
32+
'bright': 100,
33+
'color_mode': 2,
34+
'ct': 4000,
35+
'rgb': 16711680,
36+
'hue': 100,
37+
'sat': 35,
38+
'name': 'my_bulb',
39+
}
40+
41+
with Bulb(*self.bulb_addr, **kwargs) as b:
42+
assert b.fw_ver == 18
43+
assert b.support == ['get_prop', 'set_default', 'set_power', 'toggle', 'set_bright',
44+
'start_cf', 'stop_cf', 'set_scenecron_add', 'cron_get',
45+
'cron_del', 'set_ct_abx', 'set_rgb']

tests/test_discover.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import asyncio
2+
3+
import pytest
4+
5+
from yeelib.discover import YeelightProtocol, search_bulbs
6+
from yeelib.exceptions import YeelightError
7+
8+
9+
notify = b"""NOTIFY * HTTP/1.1
10+
Host: 239.255.255.250:1982
11+
Cache-Control: max-age=3600
12+
Location: yeelight://192.168.1.239:55443
13+
NTS: ssdp:alive
14+
Server: POSIX, UPnP/1.0 YGLC/1
15+
id: 0x000000000015243f
16+
model: color
17+
fw_ver: 18
18+
support: get_prop set_default set_power toggle set_bright start_cf stop_cf set_scene
19+
cron_add cron_get cron_del set_ct_abx set_rgb
20+
power: on
21+
bright: 100
22+
color_mode: 2
23+
ct: 4000
24+
rgb: 16711680
25+
hue: 100
26+
sat: 35
27+
name: my_bulb"""
28+
29+
mcast = b"""HTTP/1.1 200 OK
30+
Cache-Control: max-age=3600
31+
Date:
32+
Ext:
33+
Location: yeelight://192.168.1.239:55443
34+
Server: POSIX UPnP/1.0 YGLC/1
35+
id: 0x000000000015243f
36+
model: color
37+
fw_ver: 18
38+
support: get_prop set_default set_power toggle set_bright start_cf stop_cf set_scene
39+
cron_add cron_get cron_del set_ct_abx set_rgb
40+
power: on
41+
bright: 100
42+
color_mode: 2
43+
ct: 4000
44+
rgb: 16711680
45+
hue: 100
46+
sat: 35
47+
name: my_bulb"""
48+
49+
wrong_location = b"""HTTP/1.1 200 OK
50+
Cache-Control: max-age=3600
51+
Date:
52+
Ext:
53+
Location: yeelight://not.an.ip:55443
54+
Server: POSIX UPnP/1.0 YGLC/1"""
55+
56+
57+
class TestYeelightProtocoll:
58+
def test_notify(self, ):
59+
bulbs = {}
60+
p = YeelightProtocol(bulbs=bulbs)
61+
p.datagram_received(data=notify, addr=('192.168.1.239', 1982))
62+
assert len(bulbs) == 1
63+
assert bulbs['0x000000000015243f'].ip == '192.168.1.239'
64+
65+
def test_mcast(self, ):
66+
bulbs = {}
67+
p = YeelightProtocol(bulbs=bulbs)
68+
p.datagram_received(data=mcast, addr=('192.168.1.239', 1982))
69+
assert len(bulbs) == 1
70+
assert bulbs['0x000000000015243f'].ip == '192.168.1.239'
71+
72+
def test_duplicate(self):
73+
bulbs = {}
74+
p = YeelightProtocol(bulbs=bulbs)
75+
p.datagram_received(data=notify, addr=('192.168.1.239', 1982))
76+
p.datagram_received(data=notify, addr=('192.168.1.239', 1982))
77+
assert len(bulbs) == 1
78+
assert bulbs['0x000000000015243f'].ip == '192.168.1.239'
79+
80+
def test_wrong_location(self):
81+
bulbs = {}
82+
p = YeelightProtocol(bulbs=bulbs)
83+
with pytest.raises(YeelightError) as e:
84+
p.datagram_received(data=wrong_location, addr=('192.168.1.239', 1982))
85+
assert 'Location does not match: yeelight://not.an.ip:55443' in str(e)
86+
87+
88+
def test_search_bulbs():
89+
loop = asyncio.get_event_loop()
90+
with search_bulbs():
91+
loop.run_until_complete(asyncio.sleep(1))

yeelib/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .discover import * # NoQA
2+
from .bulbs import * # NoQA

0 commit comments

Comments
 (0)