kopia lustrzana https://github.com/Yakifo/amqtt
378 wiersze
14 KiB
Python
378 wiersze
14 KiB
Python
# Copyright (c) 2015 Nicolas JOUANIN
|
|
#
|
|
# See the file license.txt for copying permission.
|
|
|
|
import logging
|
|
import asyncio
|
|
import ssl
|
|
from urllib.parse import urlparse
|
|
|
|
from transitions import MachineError
|
|
|
|
from hbmqtt.utils import not_in_dict_or_none
|
|
from hbmqtt.session import Session
|
|
from hbmqtt.mqtt.connack import *
|
|
from hbmqtt.mqtt.connect import *
|
|
from hbmqtt.mqtt.protocol.client_handler import ClientProtocolHandler
|
|
from hbmqtt.adapters import StreamReaderAdapter, StreamWriterAdapter, WebSocketsReader, WebSocketsWriter
|
|
import websockets
|
|
|
|
_defaults = {
|
|
'keep_alive': 10,
|
|
'ping_delay': 1,
|
|
'default_qos': 0,
|
|
'default_retain': False,
|
|
}
|
|
|
|
|
|
class ClientException(BaseException):
|
|
pass
|
|
|
|
|
|
class MQTTClient:
|
|
def __init__(self, client_id=None, config=None, loop=None):
|
|
"""
|
|
|
|
:param config: Example yaml config
|
|
broker:
|
|
uri: mqtt:username@password//localhost:1883/
|
|
cafile: somefile.cert #Server authority file
|
|
capath: /some/path # certficate file path
|
|
cadata: certificate as string data
|
|
keep_alive: 60
|
|
cleansession: true
|
|
will:
|
|
retain: false
|
|
topic: some/topic
|
|
message: Will message
|
|
qos: 0
|
|
default_qos: 0
|
|
default_retain: false
|
|
topics:
|
|
a/b:
|
|
qos: 2
|
|
retain: true
|
|
:param loop:
|
|
:return:
|
|
"""
|
|
self.logger = logging.getLogger(__name__)
|
|
self.config = _defaults
|
|
if config is not None:
|
|
self.config.update(config)
|
|
if client_id is not None:
|
|
self.client_id = client_id
|
|
else:
|
|
from hbmqtt.utils import gen_client_id
|
|
self.client_id = gen_client_id()
|
|
self.logger.debug("Using generated client ID : %s" % self.client_id)
|
|
|
|
if loop is not None:
|
|
self._loop = loop
|
|
else:
|
|
self._loop = asyncio.get_event_loop()
|
|
self.session = None
|
|
self._handler = None
|
|
self._disconnect_task = None
|
|
|
|
@asyncio.coroutine
|
|
def connect(self,
|
|
uri=None,
|
|
cleansession=None,
|
|
cafile=None,
|
|
capath=None,
|
|
cadata=None):
|
|
"""
|
|
Connect to a remote broker
|
|
:param uri: Broker URI connection, conforming to `MQTT URI scheme <https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme>`_.
|
|
:param cleansession: MQTT CONNECT clean session flaf
|
|
:param cafile: server certificate authority file
|
|
:return:
|
|
"""
|
|
try:
|
|
self.session = self._initsession(uri, cleansession, cafile, capath, cadata)
|
|
self.logger.debug("Connect to: %s" % uri)
|
|
|
|
return_code = yield from self._connect_coro()
|
|
self._disconnect_task = asyncio.Task(self.handle_connection_close())
|
|
return return_code
|
|
except MachineError:
|
|
msg = "Connect call incompatible with client current state '%s'" % self.session.machine.state
|
|
self.logger.warn(msg)
|
|
self.session.machine.connect_fail()
|
|
raise ClientException(msg)
|
|
except Exception as e:
|
|
self.session.machine.disconnect()
|
|
self.logger.warn("Connection failed: %s " % e)
|
|
raise ClientException("Connection failed: %s " % e)
|
|
|
|
@asyncio.coroutine
|
|
def disconnect(self):
|
|
try:
|
|
self.session.machine.disconnect()
|
|
if not self._disconnect_task.done():
|
|
self._disconnect_task.cancel()
|
|
yield from self._handler.mqtt_disconnect()
|
|
yield from self._handler.stop()
|
|
self._handler.detach_from_session()
|
|
except MachineError as me:
|
|
if self.session.machine.state == "disconnected":
|
|
self.logger.warn("Client session is already disconnected")
|
|
else:
|
|
self.logger.debug("Invalid method call at this moment: %s" % me)
|
|
raise ClientException("Client instance can't be disconnected: %s" % me)
|
|
except Exception as e:
|
|
self.logger.warn("Unhandled exception: %s" % e)
|
|
raise ClientException("Unhandled exception: %s" % e)
|
|
|
|
@asyncio.coroutine
|
|
def reconnect(self, cleansession=False):
|
|
if self.session.machine.state == 'connected':
|
|
self.logger.warn("Client already connected")
|
|
return CONNECTION_ACCEPTED
|
|
|
|
try:
|
|
self.session.clean_session = cleansession
|
|
self.logger.debug("Reconnecting with session parameters: %s" % self.session)
|
|
|
|
return_code = yield from self._connect_coro()
|
|
self._disconnect_task = asyncio.Task(self.handle_connection_close())
|
|
return return_code
|
|
except MachineError:
|
|
msg = "Connect call incompatible with client current state '%s'" % self.session.machine.state
|
|
self.logger.warn(msg)
|
|
self.session.machine.disconnect()
|
|
raise ClientException(msg)
|
|
except Exception as e:
|
|
self.session.machine.disconnect()
|
|
self.logger.warn("Connection failed: %s " % e)
|
|
raise ClientException("Connection failed: %s " % e)
|
|
|
|
@asyncio.coroutine
|
|
def ping(self):
|
|
"""
|
|
Send a MQTT ping request and wait for response
|
|
:return: None
|
|
"""
|
|
if self.session.machine.state == 'connected':
|
|
yield from self._handler.mqtt_ping()
|
|
else:
|
|
self.logger.warn("MQTT PING request incompatible with current session state '%s'" %
|
|
self.session.machine.state)
|
|
|
|
@asyncio.coroutine
|
|
def publish(self, topic, message, qos=None, retain=None):
|
|
def get_retain_and_qos():
|
|
if qos:
|
|
_qos = qos
|
|
else:
|
|
_qos = self.config['default_qos']
|
|
try:
|
|
_qos = self.config['topics'][topic]['qos']
|
|
except KeyError:
|
|
pass
|
|
if retain:
|
|
_retain = retain
|
|
else:
|
|
_retain = self.config['default_retain']
|
|
try:
|
|
_retain = self.config['topics'][topic]['retain']
|
|
except KeyError:
|
|
pass
|
|
return _qos, _retain
|
|
(app_qos, app_retain) = get_retain_and_qos()
|
|
if app_qos == 0:
|
|
yield from self._handler.mqtt_publish(topic, message, 0x00, app_retain)
|
|
if app_qos == 1:
|
|
yield from self._handler.mqtt_publish(topic, message, 0x01, app_retain)
|
|
if app_qos == 2:
|
|
yield from self._handler.mqtt_publish(topic, message, 0x02, app_retain)
|
|
|
|
@asyncio.coroutine
|
|
def subscribe(self, topics):
|
|
return (yield from self._handler.mqtt_subscribe(topics, self.session.next_packet_id))
|
|
|
|
@asyncio.coroutine
|
|
def unsubscribe(self, topics):
|
|
yield from self._handler.mqtt_unsubscribe(topics, self.session.next_packet_id)
|
|
|
|
@asyncio.coroutine
|
|
def deliver_message(self):
|
|
return (yield from self._handler.mqtt_deliver_next_message())
|
|
|
|
@asyncio.coroutine
|
|
def acknowledge_delivery(self, packet_id):
|
|
yield from self._handler.mqtt_acknowledge_delivery(packet_id)
|
|
|
|
@asyncio.coroutine
|
|
def _connect_coro(self):
|
|
sc = None
|
|
reader = None
|
|
writer = None
|
|
kwargs = dict()
|
|
|
|
# Decode URI attributes
|
|
uri_attributes = urlparse(self.session.broker_uri)
|
|
scheme = uri_attributes.scheme
|
|
self.session.username = uri_attributes.username
|
|
self.session.password = uri_attributes.password
|
|
self.session.remote_address = uri_attributes.hostname
|
|
self.session.remote_port = uri_attributes.port
|
|
if scheme in ('mqtt', 'mqtts') and not self.session.remote_port:
|
|
self.session.remote_port = 8883 if scheme == 'mqtts' else 1883
|
|
|
|
if scheme in ('mqtts', 'wss'):
|
|
if self.session.cafile is None or self.session.cafile == '':
|
|
self.logger.warn("TLS connection can't be estabilshed, no certificate file (.cert) given")
|
|
raise ClientException("TLS connection can't be estabilshed, no certificate file (.cert) given")
|
|
sc = ssl.create_default_context(
|
|
ssl.Purpose.SERVER_AUTH,
|
|
cafile=self.session.cafile,
|
|
capath=self.session.capath,
|
|
cadata=self.session.cadata)
|
|
if 'certfile' in self.config and 'keyfile' in self.config:
|
|
sc.load_cert_chain(self.config['certfile'], self.config['keyfile'])
|
|
kwargs['ssl'] = sc
|
|
|
|
# Open connection
|
|
try:
|
|
if scheme in ('mqtt', 'mqtts'):
|
|
conn_reader, conn_writer = \
|
|
yield from asyncio.open_connection(self.session.remote_address, self.session.remote_port, **kwargs)
|
|
reader = StreamReaderAdapter(conn_reader)
|
|
writer = StreamWriterAdapter(conn_writer)
|
|
elif scheme in ('ws', 'wss'):
|
|
websocket = yield from websockets.connect(self.session.broker_uri, subprotocols=['mqtt'], **kwargs)
|
|
reader = WebSocketsReader(websocket)
|
|
writer = WebSocketsWriter(websocket)
|
|
except Exception as e:
|
|
self.logger.warn("connection failed: %s" % e)
|
|
self.session.machine.disconnect()
|
|
raise ClientException("connection Failed: %s" % e)
|
|
|
|
connect_packet = self.build_connect_packet()
|
|
yield from connect_packet.to_stream(writer)
|
|
self.logger.debug(" -out-> " + repr(connect_packet))
|
|
try :
|
|
connack = yield from ConnackPacket.from_stream(reader)
|
|
self.logger.debug(" <-in-- " + repr(connack))
|
|
return_code = connack.variable_header.return_code
|
|
|
|
if return_code is not CONNECTION_ACCEPTED:
|
|
yield from self._handler.stop()
|
|
self.session.machine.disconnect()
|
|
self.logger.warn("Connection rejected with code '%s'" % return_code)
|
|
else:
|
|
# Handle MQTT protocol
|
|
self._handler = ClientProtocolHandler(reader, writer, loop=self._loop)
|
|
self._handler.attach_to_session(self.session)
|
|
yield from self._handler.start()
|
|
self.session.machine.connect()
|
|
self.logger.debug("connected to %s:%s" % (self.session.remote_address, self.session.remote_port))
|
|
return return_code
|
|
except Exception as e:
|
|
self.logger.warn("connection failed: %s" % e)
|
|
self.session.machine.disconnect()
|
|
raise ClientException("connection Failed: %s" % e)
|
|
|
|
def build_connect_packet(self):
|
|
vh = ConnectVariableHeader()
|
|
payload = ConnectPayload()
|
|
|
|
vh.keep_alive = self.session.keep_alive
|
|
vh.clean_session_flag = self.session.clean_session
|
|
vh.will_retain_flag = self.session.will_retain
|
|
payload.client_id = self.session.client_id
|
|
|
|
if self.session.username:
|
|
vh.username_flag = True
|
|
payload.username = self.session.username
|
|
else:
|
|
vh.username_flag = False
|
|
|
|
if self.session.password:
|
|
vh.password_flag = True
|
|
payload.password = self.session.password
|
|
else:
|
|
vh.password_flag = False
|
|
if self.session.will_flag:
|
|
vh.will_flag = True
|
|
vh.will_qos = self.session.will_qos
|
|
payload.will_message = self.session.will_message
|
|
payload.will_topic = self.session.will_topic
|
|
else:
|
|
vh.will_flag = False
|
|
|
|
header = MQTTFixedHeader(CONNECT, 0x00)
|
|
packet = ConnectPacket(header, vh, payload)
|
|
return packet
|
|
|
|
@asyncio.coroutine
|
|
def handle_connection_close(self):
|
|
self.logger.debug("Watch broker disconnection")
|
|
yield from self._handler.wait_disconnect()
|
|
self.logger.debug("Handle broker disconnection")
|
|
yield from self._handler.stop()
|
|
self.session.machine.disconnect()
|
|
# while self.session.machine.state != 'connected':
|
|
# yield from asyncio.sleep(2)
|
|
# self.logger.debug("Trying reconnect")
|
|
# try:
|
|
# yield from self.reconnect()
|
|
# except ClientException:
|
|
# self.logger.warn("Reconnect failed")
|
|
|
|
def _initsession(
|
|
self,
|
|
uri=None,
|
|
cleansession=None,
|
|
cafile=None,
|
|
capath=None,
|
|
cadata=None) -> Session:
|
|
# Load config
|
|
broker_conf = self.config.get('broker', dict()).copy()
|
|
if uri:
|
|
broker_conf['uri'] = uri
|
|
if cafile:
|
|
broker_conf['cafile'] = cafile
|
|
elif 'cafile' not in broker_conf:
|
|
broker_conf['cafile'] = None
|
|
if capath:
|
|
broker_conf['capath'] = capath
|
|
elif 'capath' not in broker_conf:
|
|
broker_conf['capath'] = None
|
|
if cadata:
|
|
broker_conf['cadata'] = cadata
|
|
elif 'cadata' not in broker_conf:
|
|
broker_conf['cadata'] = None
|
|
|
|
if cleansession is not None:
|
|
broker_conf['cleansession'] = cleansession
|
|
|
|
for key in ['uri']:
|
|
if not_in_dict_or_none(broker_conf, key):
|
|
raise ClientException("Missing connection parameter '%s'" % key)
|
|
|
|
s = Session()
|
|
s.broker_uri = uri
|
|
s.client_id = self.client_id
|
|
s.cafile = broker_conf['cafile']
|
|
s.capath = broker_conf['capath']
|
|
s.cadata = broker_conf['cadata']
|
|
if cleansession is not None:
|
|
s.clean_session = cleansession
|
|
else:
|
|
s.clean_session = self.config.get('cleansession', True)
|
|
s.keep_alive = self.config['keep_alive'] - self.config['ping_delay']
|
|
if 'will' in self.config:
|
|
s.will_flag = True
|
|
s.will_retain = self.config['will']['retain']
|
|
s.will_topic = self.config['will']['topic']
|
|
s.will_message = self.config['will']['message']
|
|
s.will_qos = self.config['will']['qos']
|
|
else:
|
|
s.will_flag = False
|
|
s.will_retain = False
|
|
s.will_topic = None
|
|
s.will_message = None
|
|
return s
|