Skip to content

Commit

Permalink
Merge 62d6d84 into 2e63681
Browse files Browse the repository at this point in the history
  • Loading branch information
rytilahti committed Dec 12, 2018
2 parents 2e63681 + 62d6d84 commit 62b27ce
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 59 deletions.
2 changes: 1 addition & 1 deletion miio/ceil_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def cli(ctx, ip: str, token: str, debug: int):
@cli.command()
def discover():
"""Search for plugs in the network."""
miio.Ceil.discover()
miio.Ceil.send_handshake()


@cli.command()
Expand Down
95 changes: 52 additions & 43 deletions miio/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
import socket
from enum import Enum
from typing import Any, List, Optional # noqa: F401
from typing import Any, List, Optional, Union # noqa: F401

import click
import construct
Expand Down Expand Up @@ -53,11 +53,12 @@ def __init__(self, data):
self.data = data

def __repr__(self):
return "%s v%s (%s) @ %s - token: %s" % (
return "%s v%s (%s) @ %s (%s) - token: %s" % (
self.data["model"],
self.data["fw_ver"],
self.data["mac"],
self.network_interface["localIp"],
self.ssid,
self.data["token"])

def __json__(self):
Expand All @@ -73,6 +74,21 @@ def accesspoint(self):
"""Information about connected wlan accesspoint."""
return self.data["ap"]

@property
def ssid(self) -> Optional[str]:
"""SSID of the connected wlan accesspoint."""
return self.accesspoint["ssid"] or None

@property
def rssi(self) -> int:
"""RSSI of the wifi connection"""
return self.accesspoint["rssi"]

@property
def is_configured(self) -> bool:
"""If the device is TODO paired"""
return self.ssid is not None

@property
def model(self) -> Optional[str]:
"""Model string if available."""
Expand Down Expand Up @@ -125,8 +141,10 @@ def __init__(self, ip: str = None, token: str = None,
self.ip = ip
self.port = 54321
if token is None:
token = 32 * '0'
if token is not None:
self.token = 32 * '0'
elif isinstance(token, bytes):
self.token = token
else:
self.token = bytes.fromhex(token)
self.debug = debug
self.lazy_discover = lazy_discover
Expand All @@ -137,7 +155,7 @@ def __init__(self, ip: str = None, token: str = None,
self.__id = start_id
self._device_id = None

def do_discover(self) -> Message:
def do_handshake(self) -> Message:
"""Send a handshake to the device,
which can be used to the device type and serial.
The handshake must also be done regularly to enable communication
Expand All @@ -146,10 +164,8 @@ def do_discover(self) -> Message:
:rtype: Message
:raises DeviceException: if the device could not be discovered."""
m = Device.discover(self.ip)
m = Device.send_handshake(self.ip)
if m is not None:
self._device_id = m.header.value.device_id
self._device_ts = m.header.value.ts
self._discovered = True
if self.debug > 1:
_LOGGER.debug(m)
Expand All @@ -163,52 +179,37 @@ def do_discover(self) -> Message:

return m

@staticmethod
def discover(addr: str=None) -> Any:
"""Scan for devices in the network.
This method is used to discover supported devices by sending a
handshake message to the broadcast address on port 54321.
If the target IP address is given, the handshake will be send as
an unicast packet.
@classmethod
def from_handshake_reply(cls, addr, message, device_class=None):
if device_class:
cls = device_class
dev = cls(addr[0], message.checksum)
dev._device_id = message.header.value.device_id
dev._device_ts = message.header.value.ts
return dev

:param str addr: Target IP address"""
def send_handshake(self, initialize_token=False):
timeout = 5
is_broadcast = addr is None
seen_addrs = [] # type: List[str]
if is_broadcast:
addr = '<broadcast>'
is_broadcast = True
_LOGGER.info("Sending discovery to %s with timeout of %ss..",
addr, timeout)

# magic, length 32
helobytes = bytes.fromhex(
'21310020ffffffffffffffffffffffffffffffffffffffffffffffffffffffff')

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.settimeout(timeout)
s.sendto(helobytes, (addr, 54321))
while True:
s.sendto(helobytes, (self.ip, 54321))
while True: # TODO add retry
try:
data, addr = s.recvfrom(1024)
m = Message.parse(data) # type: Message
_LOGGER.debug("Got a response: %s", m)
if not is_broadcast:
return m

if addr[0] not in seen_addrs:
_LOGGER.info(" IP %s (ID: %s) - token: %s",
addr[0],
binascii.hexlify(m.header.value.device_id).decode(),
codecs.encode(m.checksum, 'hex'))
seen_addrs.append(addr[0])
except socket.timeout:
if is_broadcast:
_LOGGER.info("Discovery done")
return # ignore timeouts on discover
self._device_id = m.header.value.device_id
self._device_ts = m.header.value.ts
if initialize_token:
self.token = m.checksum
return
except Exception as ex:
_LOGGER.warning("error while reading discover results: %s", ex)
break
raise DeviceException("Unable to do a handshake") from ex

def send(self, command: str, parameters: Any=None, retry_count=3) -> Any:
"""Build and send the given command.
Expand All @@ -221,7 +222,7 @@ def send(self, command: str, parameters: Any=None, retry_count=3) -> Any:
:raises DeviceException: if an error has occured during communication."""

if not self.lazy_discover or not self._discovered:
self.do_discover()
self.send_handshake()

cmd = {
"id": self._id,
Expand Down Expand Up @@ -344,7 +345,7 @@ def configure_wifi(self, ssid, password, uid=0, extra_params=None):
params = {"ssid": ssid, "passwd": password, "uid": uid,
**extra_params}

return self.send("miIO.config_router", params)[0]
return self.send("miIO.config_router", params)[0] == "ok"

@property
def _id(self) -> int:
Expand All @@ -358,3 +359,11 @@ def _id(self) -> int:
def raw_id(self) -> int:
"""Return the sequence id."""
return self.__id

@property
def pretty_token(self):
"""Return a pretty string presentation for a token."""
return codecs.encode(self.token, 'hex').decode()

def __repr__(self):
return "<Device %s token: %s>" % (self.ip, self.pretty_token)
95 changes: 86 additions & 9 deletions miio/discovery.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import binascii
import codecs
import inspect
import ipaddress
import logging
import netifaces
from functools import partial
from typing import Union, Callable, Dict, Optional # noqa: F401

Expand Down Expand Up @@ -82,11 +84,6 @@
} # type: Dict[str, Union[Callable, Device]]


def pretty_token(token):
"""Return a pretty string presentation for a token."""
return codecs.encode(token, 'hex').decode()


def other_package_info(info, desc):
"""Return information about another package supporting the device."""
return "%s @ %s, check %s" % (
Expand All @@ -101,12 +98,11 @@ def create_device(name: str, addr: str, device_cls: partial) -> Device:
name, device_cls.func.__name__)

dev = device_cls(ip=addr)
m = dev.do_discover()
dev.token = m.checksum
dev.send_handshake(initialize_token=True)
_LOGGER.info("Found a supported '%s' at %s - token: %s",
device_cls.func.__name__,
addr,
pretty_token(dev.token))
dev.pretty_token)
return dev


Expand All @@ -127,9 +123,10 @@ def check_and_create_device(self, info, addr) -> Optional[Device]:
return create_device(name, addr, v)
elif callable(v):
dev = Device(ip=addr)
dev.send_handshake(initialize_token=True)
_LOGGER.info("%s: token: %s",
v(info),
pretty_token(dev.do_discover().checksum))
dev.pretty_token)
return None
_LOGGER.warning("Found unsupported device %s at %s, "
"please report to developers", name, addr)
Expand Down Expand Up @@ -161,3 +158,83 @@ def discover_mdns() -> Dict[str, Device]:
browser.cancel()

return listener.found_devices

import socket
from . import Message

class MiIODiscovery:
"""MiIO broadcast discovery"""

@staticmethod
def send_handshake(addr):
"""Scan for devices in the network.
This method is used to discover supported devices by sending a
handshake message to the broadcast address on port 54321.
If the target IP address is given, the handshake will be send as
an unicast packet.
:param str addr: Target IP address
:param bool return_first: Return a single, first encountered message"""
timeout = 5
devices = []

# magic, length 32
helobytes = bytes.fromhex(
'21310020ffffffffffffffffffffffffffffffffffffffffffffffffffffffff')

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.settimeout(timeout)
s.sendto(helobytes, (addr, 54321))
while True:
try:
data, addr = s.recvfrom(1024)
m = Message.parse(data) # type: Message
_LOGGER.debug("Got a response: %s", m)

m = Device.from_handshake_reply(addr, m)

devices.append(m)
except Exception as ex:
_LOGGER.warning("error while reading discover results: %s", ex)
break

return devices

@staticmethod
def discover(timeout=5, interfaces=None):
"""Discover devices using MiIO broadcast hello.
Note, the timeout is per interface
:param timeout: how long to wait for responses
:param interfaces: list of interfaces to send broadcasts
:return:
"""
if not interfaces:
interfaces = netifaces.interfaces()

_LOGGER.debug("Available interfaces: %s", interfaces)
ipv4_broadcasts = set()
for interface in interfaces:
addresses = netifaces.ifaddresses(interface)
if netifaces.AF_INET not in addresses:
continue

v4 = addresses[netifaces.AF_INET]
for addr in v4:
if "broadcast" in addr:
ipv4_broadcasts.add(addr["broadcast"])

all_devices = []

_LOGGER.debug("Broadcast addreses: %s", ipv4_broadcasts)
for broadcast in ipv4_broadcasts:
_LOGGER.debug("Sending discovery to %s with timeout of %ss..",
broadcast, timeout)
devices = MiIODiscovery.send_handshake(addr=broadcast)
_LOGGER.debug("Got %s devices", len(devices))
if devices is None:
continue
all_devices.extend(devices)

return all_devices
2 changes: 1 addition & 1 deletion miio/philips_eyecare_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def cli(ctx, ip: str, token: str, debug: int):
@cli.command()
def discover():
"""Search for plugs in the network."""
miio.PhilipsEyecare.discover()
miio.PhilipsEyecare.send_handshake()


@cli.command()
Expand Down
2 changes: 1 addition & 1 deletion miio/plug_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def cli(ctx, ip: str, token: str, debug: int):
@cli.command()
def discover():
"""Search for plugs in the network."""
miio.ChuangmiPlug.discover()
miio.ChuangmiPlug.send_handshake()


@cli.command()
Expand Down
36 changes: 32 additions & 4 deletions miio/vacuum_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
validate_token, LiteralParamType)
from .device import UpdateState
from .updater import OneShotServer
from .exceptions import DeviceException

_LOGGER = logging.getLogger(__name__)
pass_dev = click.make_pass_decorator(miio.Device, ensure=True)
Expand Down Expand Up @@ -90,13 +91,40 @@ def cleanup(vac: miio.Vacuum, *args, **kwargs):


@cli.command()
@click.option('--handshake', type=bool, default=False)
def discover(handshake):
@click.option('--handshake', is_flag=True, default=False)
@click.argument('interfaces', type=str, required=False, nargs=-1)
def discover(handshake, interfaces):
"""Search for robots in the network."""
def configure(dev):
"""Ask for wlan configuration.
TODO move this to miiocli"""
if click.prompt("Do you want to connect the device to the wifi?"):
ssid = click.prompt("Name of the network")
password = click.prompt("Password")

try:
if dev.configure_wifi(ssid, password):
click.echo("Wifi configuration was changed successfully, "
"the device should connect to the given network")
except DeviceException as ex:
click.echo("Unable to configure the wifi connection: %s" % ex)

if handshake:
miio.Vacuum.discover()
for dev in miio.discovery.MiIODiscovery.discover(interfaces=interfaces):
click.echo("Found %s, trying to request device information.." % dev)
try:
info = dev.info()
click.echo(" Device information: %s" % info)
except DeviceException as ex:
click.echo(" Unable to request info from device: %s" % ex)
continue

if not info.is_configured:
configure(dev)

else:
miio.Discovery.discover_mdns()
for device in miio.Discovery.discover_mdns().values():
click.echo("Found %s" % device)


@cli.command()
Expand Down

0 comments on commit 62b27ce

Please sign in to comment.