Skip to content

Commit

Permalink
improved multiple-device support
Browse files Browse the repository at this point in the history
  • Loading branch information
happyleavesaoc committed Dec 15, 2015
1 parent 4610635 commit 52d7cda
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 25 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ Control Orvibo devices with Python 3. Currently supports the S20 WiFi Smart Swit
## Usage

```python
from orvibo.s20 import S20
from orvibo.s20 import S20, discover

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

Pull requests are welcome. Possible areas for improvement:

* Discover configured devices (get IPs).
* Additional Orvibo devices.
* Expand S20 functions: Timers, configuration, etc

Expand Down
87 changes: 66 additions & 21 deletions orvibo/s20.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import binascii
import logging
import socket
import threading
import time

_LOGGER = logging.getLogger(__name__)
Expand All @@ -15,6 +16,9 @@
TIMEOUT = 1.0
DISCOVERY_TIMEOUT = 1.0

# Timeout after which to renew device subscriptions
SUBSCRIPTION_TIMEOUT = 60

# Packet constants.
MAGIC = b'\x68\x64'
DISCOVERY = b'\x00\x06\x71\x61'
Expand All @@ -28,8 +32,50 @@
ON = b'\x01'
OFF = b'\x00'

# Timeout after which to renew device subscriptions
SUBSCRIPTION_TIMEOUT = 60
# Socket
_SOCKET = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Buffer
_BUFFER = {}


def _listen():
""" Listen on socket. """
while True:
data, addr = _SOCKET.recvfrom(1024)
_BUFFER[addr[0]] = data


def _setup():
""" Set up module.
Open a UDP socket, and listen in a thread.
"""
for opt in [socket.SO_BROADCAST, socket.SO_REUSEADDR, socket.SO_REUSEPORT]:
_SOCKET.setsockopt(socket.SOL_SOCKET, opt, 1)
_SOCKET.bind(('', PORT))
udp = threading.Thread(target=_listen, daemon=True)
udp.start()


def discover(timeout=DISCOVERY_TIMEOUT):
""" Discover devices on the local network.
:param timeout: Optional timeout in seconds.
:returns: Set of discovered host addresses.
"""
hosts = set()
payload = MAGIC + DISCOVERY
for _ in range(RETRIES):
_SOCKET.sendto(bytearray(payload), ('255.255.255.255', PORT))
start = time.time()
while time.time() < start + timeout:
for host, data in _BUFFER.copy().items():
if _is_discovery_response(data):
if host not in hosts:
_LOGGER.debug("Discovered device at %s", host)
hosts.add(host)
return hosts


def _is_discovery_response(data):
Expand Down Expand Up @@ -74,13 +120,7 @@ def __init__(self, host):
:param host: IP or hostname of device.
"""
self.host = host
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for opt in [socket.SO_BROADCAST, socket.SO_REUSEADDR,
socket.SO_REUSEPORT]:
self._socket.setsockopt(socket.SOL_SOCKET, opt, 1)
self._socket.bind(('', PORT))
(self._mac, self._mac_reversed) = self._discover_mac()

self._subscribe()

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

def _subscription_is_recent(self):
""" Check if subscription occurred recently.
:returns: Yes (True) or no (False)
"""
return self.last_subscribed > time.time() - SUBSCRIPTION_TIMEOUT

def _control(self, state):
Expand Down Expand Up @@ -219,24 +263,22 @@ def _udp_transact(self, payload, handler, *args,
:param broadcast: Send a broadcast instead.
:param timeout: Timeout in seconds.
"""
if self.host in _BUFFER:
del _BUFFER[self.host]
host = self.host
if broadcast:
host = '255.255.255.255'
retval = None
self._socket.settimeout(timeout)
for _ in range(RETRIES):
self._socket.sendto(bytearray(payload), (host, PORT))
while True:
try:
data, addr = self._socket.recvfrom(1024)
# From the right device?
if addr[0] == self.host:
retval = handler(data, *args)
# Return as soon as a response is received
if retval:
return retval
except socket.timeout:
break
_SOCKET.sendto(bytearray(payload), (host, PORT))
start = time.time()
while time.time() < start + timeout:
data = _BUFFER.get(self.host, None)
if data:
retval = handler(data, *args)
# Return as soon as a response is received
if retval:
return retval

def _turn_on(self):
""" Turn on the device. """
Expand All @@ -245,3 +287,6 @@ def _turn_on(self):
def _turn_off(self):
""" Turn off the device. """
self._control(OFF)


_setup()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='orvibo',
version='1.0.1',
version='1.1.0',
description='Control Orvibo products.',
url='https://github.com/happyleavesaoc/python-orvibo/',
license='MIT',
Expand Down

0 comments on commit 52d7cda

Please sign in to comment.