Initial commit
This commit is contained in:
commit
d9ceeea556
10 changed files with 462 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
__pycache__
|
||||||
|
.DS_Store
|
||||||
|
Pipfile.lock
|
||||||
13
Pipfile
Normal file
13
Pipfile
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[[source]]
|
||||||
|
|
||||||
|
url = "https://pypi.python.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
109
README.md
Normal file
109
README.md
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
Bumper
|
||||||
|
=====
|
||||||
|
|
||||||
|
A standalone implementation of the central server used by Ecovacs
|
||||||
|
Deebot cleaning robots to relay data between the robot and client.
|
||||||
|
|
||||||
|
Tested on Ecovacs Deebot M81 Pro but should be compatible with
|
||||||
|
most wifi-enabled robots that use the Ecovacs Android app.
|
||||||
|
|
||||||
|
*Please note: this software is experimental and not ready
|
||||||
|
for production use. Use at your own risk.*
|
||||||
|
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
For fun, mostly :)
|
||||||
|
|
||||||
|
But seriously, there are a serveral reasons for
|
||||||
|
elminating the central server:
|
||||||
|
|
||||||
|
1. Convenience: It works without an internet connection or if
|
||||||
|
Ecovacs servers are down
|
||||||
|
2. Performance: No need for messages to travel to Ecovacs server
|
||||||
|
and back.
|
||||||
|
3. Security: We can completely isolate the robot from the public
|
||||||
|
Internet.
|
||||||
|
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- An Ecovacs wifi-enabled robot
|
||||||
|
- A computer on your local network to run the Bumper server
|
||||||
|
- Python 3 and pipenv
|
||||||
|
- A network router that has functionality for overriding DNS queries
|
||||||
|
- A client that can connect to Bumper and talk to the robot over the
|
||||||
|
Ecovacs protocol.
|
||||||
|
I recomend [Sucks](https://github.com/wpietri/sucks).
|
||||||
|
It can run on the same computer and requires only minimal modification
|
||||||
|
to work with Bumper.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- Run `pipenv install` to install dependencies
|
||||||
|
|
||||||
|
- Configure your Ecovacs vacuum using the official mobile app
|
||||||
|
(if you haven't done this already)
|
||||||
|
|
||||||
|
- Configure your router DNS to point the domain lbo.ecovacs.net to
|
||||||
|
the machine that will run the Bumper server
|
||||||
|
|
||||||
|
- Start Bumper with `pipenv run python bumper.py`
|
||||||
|
|
||||||
|
- Reboot your robot (remove and re-insert the battery pack,
|
||||||
|
then power it on)
|
||||||
|
|
||||||
|
- If your configuration is correct, the robot will connect to Bumper
|
||||||
|
within about 30 seconds. Bumper will output informaiton about the
|
||||||
|
connection status.
|
||||||
|
|
||||||
|
- Configure your client - see below.
|
||||||
|
|
||||||
|
|
||||||
|
## Using with Sucks
|
||||||
|
|
||||||
|
Instructions (verified to work with Sucks 0.8.3)
|
||||||
|
|
||||||
|
- Download and install [Sucks](https://github.com/wpietri/sucks)
|
||||||
|
- See the [example script](examples/sucks.py) for how to connect
|
||||||
|
|
||||||
|
|
||||||
|
## Using with the official Android/iOS App
|
||||||
|
|
||||||
|
Bumper *can* be used with the official app, but with limitations. Your
|
||||||
|
phone needs to use your DNS server with custom settings, and the app
|
||||||
|
authenticates via Ecovacs central servers every time you start it.
|
||||||
|
|
||||||
|
- Configure your DNS server to point the domains msg-na.ecouser.net and
|
||||||
|
msg-ww.ecouser.net to the machine running Bumpy. Note: Depending on
|
||||||
|
country, your phone may be using a different domain.
|
||||||
|
|
||||||
|
- Login to the app. It will authenticate and ask for a list of robots
|
||||||
|
from Ecovacs central servers.
|
||||||
|
|
||||||
|
- The app will now connect to Bumper and try to ping the robot. Bumper
|
||||||
|
responds to this ping to tell the app that the robot is online.
|
||||||
|
|
||||||
|
- You should now be able to control the robot from the app.
|
||||||
|
|
||||||
|
|
||||||
|
## How does it work?
|
||||||
|
|
||||||
|
Ecovacs robots communicate over the XMPP (jabber) protocol. Messages
|
||||||
|
are relayed by a central XMPP server.
|
||||||
|
|
||||||
|
Bumper exposes a simulated XMPP server that implements the nessecary
|
||||||
|
functions for relaying messages between a robot and a client.
|
||||||
|
|
||||||
|
When the robot boots up it sends a HTTP request to lbo.ecovacs.net:8007
|
||||||
|
asking for the IP address and port of the XMPP server. Because of our
|
||||||
|
DNS override, this request will be received by Bumper. We tell the robot
|
||||||
|
to connect over XMPP to our local machine. Voilá!
|
||||||
|
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
Bumper woulden't exist without [Sucks](https://github.com/wpietri/sucks),
|
||||||
|
an open source client for Ecovacs robots. Big thanks to @wpietri and
|
||||||
|
contributors!
|
||||||
16
bumper.py
Normal file
16
bumper.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import bumper
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO,
|
||||||
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
bumper.ConfServer()
|
||||||
|
bumper.XMPPServer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
pass
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.info('keyboard interrupt')
|
||||||
4
bumper/__init__.py
Normal file
4
bumper/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from .confserver import ConfServer
|
||||||
|
from .xmppserver import XMPPServer
|
||||||
43
bumper/confserver.py
Normal file
43
bumper/confserver.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from http.server import HTTPServer
|
||||||
|
from http.server import BaseHTTPRequestHandler
|
||||||
|
from http import HTTPStatus
|
||||||
|
import socket, logging, _thread, json
|
||||||
|
|
||||||
|
|
||||||
|
class RequestHandler(BaseHTTPRequestHandler):
|
||||||
|
def do_POST(self):
|
||||||
|
try:
|
||||||
|
self.protocol_version = 'HTTP/1.1'
|
||||||
|
content_length = int(self.headers['Content-Length'])
|
||||||
|
post_data = self.rfile.read(content_length)
|
||||||
|
logging.debug("Headers: " + str(self.headers))
|
||||||
|
request_body = post_data.decode('utf-8')
|
||||||
|
logging.debug("Request: " + request_body)
|
||||||
|
if request_body.find('EcoMsgNew') > -1:
|
||||||
|
body = '{{"result":"ok","ip":"{}","port":5223}}'.format(socket.gethostbyname(socket.gethostname()))
|
||||||
|
else:
|
||||||
|
body = '{"result":"ok","ip":"47.88.66.164","port":8005}'
|
||||||
|
logging.debug("Response: " + body)
|
||||||
|
body = body.encode()
|
||||||
|
self.send_response(HTTPStatus.OK)
|
||||||
|
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
||||||
|
self.send_header('Connection', 'keep-alive')
|
||||||
|
self.send_header('Content-Length', len(body))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfServer():
|
||||||
|
def __init__(self):
|
||||||
|
try:
|
||||||
|
server_address = (socket.gethostbyname(socket.gethostname()), 8007)
|
||||||
|
httpd = HTTPServer(server_address, RequestHandler)
|
||||||
|
logging.info("ConfServer: running on http://{}:{}".format(server_address[0], server_address[1]))
|
||||||
|
_thread.start_new_thread(httpd.serve_forever, ())
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
168
bumper/xmppserver.py
Normal file
168
bumper/xmppserver.py
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys, socket, _thread, re, time, logging, uuid
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
class XMPPServer():
|
||||||
|
server_id = 'bumper'
|
||||||
|
bot_id = 'bumpy'
|
||||||
|
client_id = None
|
||||||
|
clients = []
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
try:
|
||||||
|
# Initialize bot server
|
||||||
|
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
server_address = (socket.gethostbyname(socket.gethostname()), 5223)
|
||||||
|
server.bind(server_address)
|
||||||
|
server.listen(1)
|
||||||
|
logging.info('XMPPServer: listening on {}:{}'.format(server_address[0], server_address[1]))
|
||||||
|
while True:
|
||||||
|
logging.info('XMPPServer: awaiting connection')
|
||||||
|
connection, client_address = server.accept()
|
||||||
|
# disconnect any clients with this ip
|
||||||
|
for client in self.clients:
|
||||||
|
if client.address == client_address[0]:
|
||||||
|
client.disconnect()
|
||||||
|
_thread.start_new_thread(Client,(connection, client_address))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.info('keyboard interrupt')
|
||||||
|
server.shutdown(2)
|
||||||
|
server.close()
|
||||||
|
except Exception as e:
|
||||||
|
server.shutdown(2)
|
||||||
|
server.close()
|
||||||
|
logging.error(e)
|
||||||
|
|
||||||
|
|
||||||
|
class Client():
|
||||||
|
IDLE = 0
|
||||||
|
CONNECT = 1
|
||||||
|
INIT = 2
|
||||||
|
BIND = 3
|
||||||
|
READY = 4
|
||||||
|
DISCONNECT = 5
|
||||||
|
UNKNOWN = 0
|
||||||
|
BOT = 1
|
||||||
|
CONTROLLER = 2
|
||||||
|
|
||||||
|
def __init__(self, connection, client_address):
|
||||||
|
self.id = uuid.uuid4()
|
||||||
|
self.type = self.UNKNOWN
|
||||||
|
self.state = self.IDLE
|
||||||
|
self.connection = connection
|
||||||
|
self.address = client_address[0]
|
||||||
|
XMPPServer.clients.append(self)
|
||||||
|
self._main()
|
||||||
|
|
||||||
|
def send(self, command):
|
||||||
|
logging.debug('to {}: {}'.format(self.address, command))
|
||||||
|
self.connection.send(command.encode())
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
logging.info('{} disconnecting'.format(self.address))
|
||||||
|
self.connection.close()
|
||||||
|
self._set_state('DISCONNECT')
|
||||||
|
|
||||||
|
def _tag_strip_uri(self, tag):
|
||||||
|
if tag[0] == "{":
|
||||||
|
uri, ignore, tag = tag[1:].partition("}")
|
||||||
|
return tag
|
||||||
|
|
||||||
|
def _set_state(self, state):
|
||||||
|
new_state = getattr(Client, state)
|
||||||
|
if self.state > new_state:
|
||||||
|
raise Exception("{} illegal state change {}->{}".format(self.address, self.state, new_state))
|
||||||
|
logging.info('{} state: {}'.format(self.address, state))
|
||||||
|
self.state = new_state
|
||||||
|
|
||||||
|
def _handle_ctl(self, xml, data):
|
||||||
|
ctl = xml[0][0]
|
||||||
|
if ctl.get('admin') and self.type == self.BOT:
|
||||||
|
logging.info('admin username received from bot: {}'.format(ctl.get('admin')))
|
||||||
|
XMPPServer.client_id = ctl.get('admin')
|
||||||
|
return
|
||||||
|
# forward
|
||||||
|
for client in XMPPServer.clients:
|
||||||
|
if client.address != self.address and client.state == client.READY:
|
||||||
|
if client.type == self.BOT:
|
||||||
|
data = data.decode('utf-8')
|
||||||
|
id_index = data.find('id')
|
||||||
|
if id_index > -1:
|
||||||
|
data = data[:id_index] + 'from="' + XMPPServer.client_id + '" ' + data[id_index:]
|
||||||
|
data = data.encode()
|
||||||
|
client.send(data.decode('utf-8'))
|
||||||
|
|
||||||
|
def _handle_result(self, data):
|
||||||
|
# forward
|
||||||
|
for client in XMPPServer.clients:
|
||||||
|
if client.address != self.address and client.state == client.READY:
|
||||||
|
logging.debug('sending result: ' + data.decode('utf-8'))
|
||||||
|
client.send(data.decode('utf-8'))
|
||||||
|
|
||||||
|
def _main(self):
|
||||||
|
try:
|
||||||
|
logging.info('client connected: {}'.format(self.address))
|
||||||
|
self._set_state('CONNECT')
|
||||||
|
while True:
|
||||||
|
data = self.connection.recv(4096)
|
||||||
|
if data:
|
||||||
|
logging.debug('from {}: {}'.format(self.address, data.decode('utf-8')))
|
||||||
|
try:
|
||||||
|
if self.state == self.CONNECT:
|
||||||
|
if data.decode('utf-8').find('jabber:client') > -1:
|
||||||
|
self._set_state('INIT')
|
||||||
|
# ack jabbr:client
|
||||||
|
self.send('<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0" id="1" from="{}">'.format(XMPPServer.server_id))
|
||||||
|
time.sleep(0.5)
|
||||||
|
# session
|
||||||
|
self.send('<stream:features><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></stream:features>')
|
||||||
|
continue
|
||||||
|
xml = ET.fromstring(data)
|
||||||
|
if len(xml):
|
||||||
|
child = self._tag_strip_uri(xml[0].tag)
|
||||||
|
else:
|
||||||
|
child = None
|
||||||
|
if xml.get('id'):
|
||||||
|
last_id = xml.get('id')
|
||||||
|
else:
|
||||||
|
last_id = '0'
|
||||||
|
if xml.tag == 'iq':
|
||||||
|
res = None
|
||||||
|
if child == 'bind':
|
||||||
|
res = '<iq type="result" id="{}"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>{}</jid></bind></iq>'.format(last_id, XMPPServer.bot_id)
|
||||||
|
self._set_state('BIND')
|
||||||
|
elif child == 'session':
|
||||||
|
res = '<iq type="result" id="{}" />'.format(last_id)
|
||||||
|
self._set_state('READY')
|
||||||
|
elif child == 'query':
|
||||||
|
self._handle_ctl(xml, data)
|
||||||
|
elif child == 'ping':
|
||||||
|
# respond to ping request
|
||||||
|
res = '<iq type="result" id="{}" from="{}" />'.format(last_id, xml.get('to'))
|
||||||
|
elif xml.get('type') == 'result':
|
||||||
|
self._handle_result(data)
|
||||||
|
if res:
|
||||||
|
self.send(res)
|
||||||
|
elif xml.tag == 'presence':
|
||||||
|
if len(xml) and xml[0].tag == 'status':
|
||||||
|
# bot announcing arrival
|
||||||
|
self.type = self.BOT
|
||||||
|
logging.info('{} type set to BOT (based on presence tag)'.format(self.address))
|
||||||
|
# send a command from an unknown user - the response will contain the correct admin username
|
||||||
|
self.send('<iq type="set" id="{}" from="{}" to="{}"><query xmlns="com:ctl"><ctl td="GetCleanState" /></query></iq>'.format(uuid.uuid4(), 'unknown@ecouser.net', XMPPServer.bot_id))
|
||||||
|
elif xml.get('type') == 'available':
|
||||||
|
self.type = self.CONTROLLER
|
||||||
|
logging.info('{} type set to CONTROLLER (based on presence tag)'.format(self.address))
|
||||||
|
except ET.ParseError as e:
|
||||||
|
logging.debug("error: {}".format(e))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
self._set_state('DISCONNECT')
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
self._set_state('DISCONNECT')
|
||||||
|
finally:
|
||||||
|
self.disconnect()
|
||||||
33
certs/cert.pem
Normal file
33
certs/cert.pem
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFtTCCA52gAwIBAgIJAKLG5udzkR+zMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
||||||
|
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
||||||
|
aWRnaXRzIFB0eSBMdGQwHhcNMTcxMjA1MTkxNTQwWhcNMTgxMjA1MTkxNTQwWjBF
|
||||||
|
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
||||||
|
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
|
||||||
|
CgKCAgEAu1z0Iw7KLJZD8CBrZWm9Jp7eqIAqdoTn3GWmNmzgIDnzmxZb70War74d
|
||||||
|
7aWDVtEVtOZQ8ShW15zRanr6hQkHdM5Li1KTMYW3P7TOmzJZkESNEZviOyvlfRPG
|
||||||
|
oLLbPIp+A7W8Zz3ST0cDzyy6N7LRAA0OywRgPhrxrN637Fx1+SFwqKQg0IUOuX3v
|
||||||
|
veZ1oAZSBUDzpDPUaak7jXugB11wt6XTEekwOcrH7WEDoFH87thtJyCM7IidW0bM
|
||||||
|
rkqxeiM18qMzn8cGqnm2nY+VHgK0t0/QZ/cftPVc2IHZLr/+eTGaJmPv7gL+7v/l
|
||||||
|
7fgk3RMTn0GrdTO+Vvnu1qNK7r+//KMgGOo6zwEQAI9Ml6y1LRuEXrymgsYY9qCE
|
||||||
|
ZtBV0FoZaVYAuGG4cvCDLLPgM2OivIcWgG+77MqMH/RkWg+BrUesn7ISEpF+7Q7+
|
||||||
|
jc6pFYr4y5fc2jJ9/c5Mxu6sv0Tc8N/c9WpcXsJ6Y1JrBbDcdGSwmTZ1JVyiog9O
|
||||||
|
6bU/16QTuU3w2oH9qjnc907tZuB66P53WJ2iHPvB8yF5V93FvtbPYsHpO7noURSI
|
||||||
|
LqvKoNy31ndv4kao2AIzrJuhBBkSorBEcoFerRRBJMHa85R5Tb6iuGg6RFv0BB8K
|
||||||
|
gv6mG/7oqmR8QDnRRd54U2pEsIkYmgoIU4/8L91r9Vd1eiEDEnsCAwEAAaOBpzCB
|
||||||
|
pDAdBgNVHQ4EFgQUqA/GBsZAepFIcbgw6gK/HTUWolswdQYDVR0jBG4wbIAUqA/G
|
||||||
|
BsZAepFIcbgw6gK/HTUWoluhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
|
||||||
|
b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCi
|
||||||
|
xubnc5EfszAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQB7kHEHKNDQ
|
||||||
|
TcvwY9z3RscpHyWwjrGjUdjk8AkM9cTm/sT8G7JU/7+KZueu4elNOc4hir/+DMlO
|
||||||
|
/AN6IXRNrfPO8IVG6hKnK9K3wvceJzg6mT8F4peg4vMNdIsrZyQaPhWArUF7LG/Q
|
||||||
|
HF4Yk8jnw3NKIG5fnlagAGFDdDGSdDK/IQN+JA6sHJVQRrBLpksRUCWFY7QJGYVS
|
||||||
|
DmEAYr6Y6emftbflUjW6iC/E57lC5F7bQyoIEIBBHKjMaNTdM8TzOUZSOGLwAuVD
|
||||||
|
C1iAoZn7VWeGa4h/xwx0yio0yeI6gRh3fINBKoCNv6pLUm6sgrW8oqGMDJiH0T3H
|
||||||
|
sc8xC3bK2DPO4sIyisL3HUPcsSFB/hf0i8BOQrihrpZEn/fx4lTRyQYSKT5NVSl4
|
||||||
|
v/9hmDjWIXh9EInGfP8DdWp4Og8GiXcV9rUqvRBo5aBwqK/aTNRCk6ah5N958yFu
|
||||||
|
wHlrbQmPnw1AqJSG1oG+4dcXnrcgd/xGMAarTbgzqsd8icafg8v2x2pG5ZexE3uN
|
||||||
|
0METK9hvCrjN8/Rvo2ViTDa8WF4CXMuVFG4FdbeRrYPZl1j1jBYDMtKkVJJCsVQd
|
||||||
|
chYbqKgNDmMcbfpksBtF3XpYTtVNAKA9g5mrAhEJS88uy63PStLoBQNXv9y8ksql
|
||||||
|
VVCTFNGy5JRyvOeSRYSSdXjKbfj5Vgh1HQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
51
certs/key.pem
Normal file
51
certs/key.pem
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
22
examples/sucks.py
Normal file
22
examples/sucks.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from sucks import *
|
||||||
|
|
||||||
|
class BumperVacBot(VacBot):
|
||||||
|
def __init__(self, server_address):
|
||||||
|
self.server_address = server_address
|
||||||
|
vacuum = { 'did':'none','class':'none' }
|
||||||
|
super().__init__('sucks', 'ecouser.net', '', '', vacuum, '')
|
||||||
|
def connect_and_wait_until_ready(self):
|
||||||
|
logging.info('connecting')
|
||||||
|
self.xmpp.connect(self.server_address)
|
||||||
|
self.xmpp.process()
|
||||||
|
self.xmpp.wait_until_ready()
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
# Change server address to the machine running Bumper
|
||||||
|
server_address = ('xxx.xxx.xxx.xxx', 5223)
|
||||||
|
|
||||||
|
bumper_client = BumperVacBot(server_address)
|
||||||
|
bumper_client.connect_and_wait_until_ready()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue