Skip to content

Commit 52d7cda

Browse files
improved multiple-device support
1 parent 4610635 commit 52d7cda

File tree

3 files changed

+70
-25
lines changed

3 files changed

+70
-25
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ Control Orvibo devices with Python 3. Currently supports the S20 WiFi Smart Swit
55
## Usage
66

77
```python
8-
from orvibo.s20 import S20
8+
from orvibo.s20 import S20, discover
99

10-
s20 = S20("x.x.x.x") # Discover the IP on your own.
10+
hosts = discover() # Discover devices on your local network.
11+
s20 = S20("x.x.x.x") # Use a discovered host, or supply a known host.
1112
print(s20.on) # Current state (True = ON, False = OFF).
1213
s20.on = True # Turn it on.
1314
s20.on = False # Turn it off.
@@ -17,7 +18,6 @@ s20.on = False # Turn it off.
1718

1819
Pull requests are welcome. Possible areas for improvement:
1920

20-
* Discover configured devices (get IPs).
2121
* Additional Orvibo devices.
2222
* Expand S20 functions: Timers, configuration, etc
2323

orvibo/s20.py

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import binascii
44
import logging
55
import socket
6+
import threading
67
import time
78

89
_LOGGER = logging.getLogger(__name__)
@@ -15,6 +16,9 @@
1516
TIMEOUT = 1.0
1617
DISCOVERY_TIMEOUT = 1.0
1718

19+
# Timeout after which to renew device subscriptions
20+
SUBSCRIPTION_TIMEOUT = 60
21+
1822
# Packet constants.
1923
MAGIC = b'\x68\x64'
2024
DISCOVERY = b'\x00\x06\x71\x61'
@@ -28,8 +32,50 @@
2832
ON = b'\x01'
2933
OFF = b'\x00'
3034

31-
# Timeout after which to renew device subscriptions
32-
SUBSCRIPTION_TIMEOUT = 60
35+
# Socket
36+
_SOCKET = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
37+
38+
# Buffer
39+
_BUFFER = {}
40+
41+
42+
def _listen():
43+
""" Listen on socket. """
44+
while True:
45+
data, addr = _SOCKET.recvfrom(1024)
46+
_BUFFER[addr[0]] = data
47+
48+
49+
def _setup():
50+
""" Set up module.
51+
52+
Open a UDP socket, and listen in a thread.
53+
"""
54+
for opt in [socket.SO_BROADCAST, socket.SO_REUSEADDR, socket.SO_REUSEPORT]:
55+
_SOCKET.setsockopt(socket.SOL_SOCKET, opt, 1)
56+
_SOCKET.bind(('', PORT))
57+
udp = threading.Thread(target=_listen, daemon=True)
58+
udp.start()
59+
60+
61+
def discover(timeout=DISCOVERY_TIMEOUT):
62+
""" Discover devices on the local network.
63+
64+
:param timeout: Optional timeout in seconds.
65+
:returns: Set of discovered host addresses.
66+
"""
67+
hosts = set()
68+
payload = MAGIC + DISCOVERY
69+
for _ in range(RETRIES):
70+
_SOCKET.sendto(bytearray(payload), ('255.255.255.255', PORT))
71+
start = time.time()
72+
while time.time() < start + timeout:
73+
for host, data in _BUFFER.copy().items():
74+
if _is_discovery_response(data):
75+
if host not in hosts:
76+
_LOGGER.debug("Discovered device at %s", host)
77+
hosts.add(host)
78+
return hosts
3379

3480

3581
def _is_discovery_response(data):
@@ -74,13 +120,7 @@ def __init__(self, host):
74120
:param host: IP or hostname of device.
75121
"""
76122
self.host = host
77-
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
78-
for opt in [socket.SO_BROADCAST, socket.SO_REUSEADDR,
79-
socket.SO_REUSEPORT]:
80-
self._socket.setsockopt(socket.SOL_SOCKET, opt, 1)
81-
self._socket.bind(('', PORT))
82123
(self._mac, self._mac_reversed) = self._discover_mac()
83-
84124
self._subscribe()
85125

86126
@property
@@ -144,6 +184,10 @@ def _subscribe(self):
144184
"No status could be found for {}".format(self.host))
145185

146186
def _subscription_is_recent(self):
187+
""" Check if subscription occurred recently.
188+
189+
:returns: Yes (True) or no (False)
190+
"""
147191
return self.last_subscribed > time.time() - SUBSCRIPTION_TIMEOUT
148192

149193
def _control(self, state):
@@ -219,24 +263,22 @@ def _udp_transact(self, payload, handler, *args,
219263
:param broadcast: Send a broadcast instead.
220264
:param timeout: Timeout in seconds.
221265
"""
266+
if self.host in _BUFFER:
267+
del _BUFFER[self.host]
222268
host = self.host
223269
if broadcast:
224270
host = '255.255.255.255'
225271
retval = None
226-
self._socket.settimeout(timeout)
227272
for _ in range(RETRIES):
228-
self._socket.sendto(bytearray(payload), (host, PORT))
229-
while True:
230-
try:
231-
data, addr = self._socket.recvfrom(1024)
232-
# From the right device?
233-
if addr[0] == self.host:
234-
retval = handler(data, *args)
235-
# Return as soon as a response is received
236-
if retval:
237-
return retval
238-
except socket.timeout:
239-
break
273+
_SOCKET.sendto(bytearray(payload), (host, PORT))
274+
start = time.time()
275+
while time.time() < start + timeout:
276+
data = _BUFFER.get(self.host, None)
277+
if data:
278+
retval = handler(data, *args)
279+
# Return as soon as a response is received
280+
if retval:
281+
return retval
240282

241283
def _turn_on(self):
242284
""" Turn on the device. """
@@ -245,3 +287,6 @@ def _turn_on(self):
245287
def _turn_off(self):
246288
""" Turn off the device. """
247289
self._control(OFF)
290+
291+
292+
_setup()

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name='orvibo',
5-
version='1.0.1',
5+
version='1.1.0',
66
description='Control Orvibo products.',
77
url='https://github.com/happyleavesaoc/python-orvibo/',
88
license='MIT',

0 commit comments

Comments
 (0)