├── .gitignore ├── LICENSE ├── README.md ├── __main__.py ├── commands.py ├── config.json ├── control.py ├── control_gpio.py ├── duco.py ├── ducobox.py ├── ducobox_serial.py ├── paho ├── __init__.py └── mqtt │ ├── __init__.py │ ├── client.py │ ├── matcher.py │ ├── publish.py │ └── subscribe.py ├── py-duco-mqtt.service ├── test.py ├── utils.py └── wire-diagram.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Marten Jacobs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Ducobox MQTT bridge 2 | 3 | This package allows for communication between a Ducobox ventilation unit and an MQTT service. It was tested using [Home Assistant](http://www.home-assistant.io)'s built-in MQTT broker. 4 | 5 | ## Supported Ducobox communication protocols 6 | This package supports the use of a different communication protocol for reading of parameters and control of the unit. 7 | Currently, only direct serial communication is supported for reading, and only GPIO is supported for control, but implementing further types is pretty easy. I'm open to pull requests. 8 | 9 | # Connector cable 10 | There is a 5-pins serial connector on the Ducobox main board. You can create a simple cable that allows for direct communication from a Raspberry Pi or a serial UART. The port is talked about in-depth in [this forum thread](https://gathering.tweakers.net/forum/list_messages/1724875) (in Dutch). Apparently the port uses 3.3V TTL, but is 5V tolerant. The pins are (from right to left): 11 | 1. TX (from Duco) 12 | 2. RX (to Duco) 13 | 3. Unknown 14 | 4. GND 15 | 5. Vcc (3.3V) 16 | 17 | To build a cable, I used the following parts: 18 | - 1x JST PHR-5 (connector) 19 | - 5x JST SPH-002T-P0.5S (crimp socket) 20 | - 1x MOLEX 50-57-9405 (connector) 21 | - 5x MOLEX 16-02-0107 (crimp pin) 22 | - ca 30 cm 3M 3365-10 (ribbon-kabel) 23 | 24 | # Control from GPIO 25 | Control of the box is not possible through the serial protocol (this was confirmed to me by Duco). So, to get around this, I added a two-channel relay board that emulates a manual 3-position switch (section 5A in the [Quick Start Guide](https://www.duco.eu/Wes/CDN/1/Attachments/Quick-Start-DucoBox-Silent-17xxxx_636506783259119017.pdf)). This allows setting the box to modes CNT1, CNT2 and CNT3. I wired it up like this: 26 | 27 | ![Wire diagram](wire-diagram.png?raw=true) 28 | 29 | L1 goes to the power input that also goes to the main board of the ducobox, and L2 and L3 go to the 3-position switch ports, as found in the QSG. The relays are controlled by GPIO 17 and 27. With the board I'm using, the relays are active-low (they're flipped when the GPIO is driven low). 30 | 31 | ## Supported MQTT brokers 32 | The MQTT client used is [paho](https://www.eclipse.org/paho/). It's one of the most widely-used MQTT clients for Python, so it should work on most brokers. If you're having problems with a certain type, please open an issue or send me a pull request with a fix. 33 | 34 | ## Configuration 35 | The configuration for the bridge is located in config.json. 36 | 37 | ### Example configuration 38 | ```json 39 | { 40 | "duco" : { 41 | "type": "serial", 42 | "device": "/dev/serial0", 43 | "baudrate": 115200 44 | }, 45 | "control": { 46 | "type": "gpio", 47 | "active_low": true, 48 | "states": { 49 | "CNT1" : { 50 | "17": 0, 51 | "27": 0 52 | }, 53 | "CNT2" : { 54 | "17": 1, 55 | "27": 0 56 | }, 57 | "CNT3" : { 58 | "17": 0, 59 | "27": 1 60 | } 61 | } 62 | }, 63 | "mqtt" : { 64 | "client_id": "duco", 65 | "host": "127.0.0.1", 66 | "port": 1883, 67 | "keepalive": 60, 68 | "bind_address": "", 69 | "username": null, 70 | "password": null, 71 | "qos": 0, 72 | "pub_topic_namespace": "value/duco", 73 | "sub_topic_namespace": "set/duco", 74 | "retain": true 75 | } 76 | } 77 | ``` 78 | 79 | ## Installation 80 | To install this script as a daemon, run the following commands (on a Debian-based distribution): 81 | 82 | 1. Install dependencies: 83 | ```bash 84 | sudo apt install python python-serial 85 | ``` 86 | 2. Create a new folder, for example: 87 | ```bash 88 | sudo mkdir -p /usr/lib/py-duco-mqtt 89 | cd /usr/lib/py-duco-mqtt 90 | ``` 91 | 3. Clone this repository into the current directory: 92 | ```bash 93 | sudo git clone https://github.com/martenjacobs/py-duco-mqtt.git . 94 | ``` 95 | 4. Change `config.json` with your favorite text editor 96 | 5. Copy the service file to the systemd directory. If you used a different folder name than `/usr/lib/py-duco-mqtt` you will need to change the `WorkingDirectory` in the file first. 97 | ```bash 98 | sudo cp ./py-duco-mqtt.service /etc/systemd/system/ 99 | ``` 100 | 6. Enable the service so it starts up on boot: 101 | ```bash 102 | sudo systemctl daemon-reload 103 | sudo systemctl enable py-duco-mqtt.service 104 | ``` 105 | 7. Start up the service 106 | ```bash 107 | sudo systemctl start py-duco-mqtt.service 108 | ``` 109 | 8. View the log to see if everything works 110 | ```bash 111 | journalctl -u py-duco-mqtt.service -f 112 | ``` 113 | 114 | ## Topics 115 | 116 | ### Publish topics 117 | By default, the service publishes messages to the following MQTT topics: 118 | 119 | - value/duco _=> service on-line status_ 120 | - value/duco/network/1/stat _=> state_ 121 | - value/duco/network/1/info _=> info_ 122 | - value/duco/network/4/stts _=> stts_ 123 | - value/duco/network/1/%dbt _=> dbt (%)_ 124 | - value/duco/network/1/trgt _=> target value (???)_ 125 | - value/duco/network/1/cntdwn _=> countdown (s)_ 126 | - value/duco/temp _=> temperature (ºC)_ 127 | - value/duco/co2 _=> CO2 (ppm)_ 128 | - value/duco/humi _=> Humidity (%)_ 129 | - value/duco/fan/Filtered _=> filtered fan speed (rpm)_ 130 | - value/duco/fan/Actual _=> actual fan speed (rpm)_ 131 | 132 | > If you've changed the pub_topic_namespace value in the configuration, replace `value/duco` with your configured value. 133 | > __TODO:__ Improve description of all topics 134 | 135 | ### Subscription topics 136 | By default, the service listens to messages from the following MQTT topics: 137 | 138 | - set/duco/state _=> state_ 139 | 140 | > If you've changed the sub_topic_namespace value in the configuration, replace `set/duco` with your configured value. 141 | > __TODO:__ Improve description of all topics 142 | 143 | # Home Assistant 144 | The following configuration can be used in Home Assistant: 145 | ```yaml 146 | sensor duco: 147 | - platform: mqtt 148 | state_topic: "value/duco" 149 | name: "DUCO service on-line" 150 | - platform: mqtt 151 | state_topic: "value/duco/network/1/stat" 152 | name: "DUCO state" 153 | - platform: mqtt 154 | state_topic: "value/duco/network/1/info" 155 | name: "DUCO info" 156 | - platform: mqtt 157 | state_topic: "value/duco/network/4/stts" 158 | name: "DUCO stts" 159 | - platform: mqtt 160 | state_topic: "value/duco/network/1/%dbt" 161 | name: "DUCO dbt" 162 | unit_of_measurement: '%' 163 | - platform: mqtt 164 | state_topic: "value/duco/network/1/trgt" 165 | name: "DUCO target value" 166 | unit_of_measurement: '???' 167 | - platform: mqtt 168 | state_topic: "value/duco/network/1/cntdwn" 169 | name: "DUCO countdown" 170 | unit_of_measurement: 's' 171 | - platform: mqtt 172 | state_topic: "value/duco/temp" 173 | name: "DUCO temperature" 174 | unit_of_measurement: 'ºC' 175 | - platform: mqtt 176 | state_topic: "value/duco/co2" 177 | name: "DUCO CO2" 178 | unit_of_measurement: 'ppm' 179 | - platform: mqtt 180 | state_topic: "value/duco/humi" 181 | name: "DUCO Humidity" 182 | unit_of_measurement: '%' 183 | - platform: mqtt 184 | state_topic: "value/duco/fan/Filtered" 185 | name: "DUCO filtered fan speed" 186 | unit_of_measurement: 'rpm' 187 | - platform: mqtt 188 | state_topic: "value/duco/fan/Actual" 189 | name: "DUCO actual fan speed" 190 | unit_of_measurement: 'rpm' 191 | 192 | fan mqtt: 193 | - platform: mqtt 194 | name: "Ducobox" 195 | command_topic: "set/duco" 196 | state_topic: "value/duco" 197 | payload_on: "online" 198 | payload_off: "offline" 199 | speed_state_topic: "value/duco/network/1/stat" 200 | speed_command_topic: "set/duco/state" 201 | qos: 0 202 | payload_low_speed: "CNT1" 203 | payload_medium_speed: "CNT2" 204 | payload_high_speed: "CNT3" 205 | speeds: 206 | - low 207 | - medium 208 | - high 209 | ``` 210 | -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | from duco import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /commands.py: -------------------------------------------------------------------------------- 1 | from utils import merge 2 | import re 3 | 4 | def get_para(target, node, para, timeout=1): 5 | data = target.run_command("nodeparaget {} {}".format(node, para), timeout=timeout) 6 | return data[2][4:] 7 | 8 | 9 | def get_temperature(target): 10 | return int(get_para(target, 4, 73))/10.0 # degC 11 | 12 | def get_co2(target): 13 | return int(get_para(target, 4, 74)) # ppm 14 | 15 | def get_humidity(target): 16 | return int(get_para(target, 4, 75))/100.0 # % 17 | 18 | def get_network_data(target): 19 | line_regex = re.compile(r'^((?:[^\|]+?\|){19}[^\|]+?)$') 20 | field_regex = re.compile(r'([^ \|\n]+) *(?:\||$)') 21 | 22 | fields = \ 23 | list( 24 | list( 25 | i.group(1) for i in field_regex.finditer(l) 26 | ) 27 | for l 28 | in ( 29 | i.group(1) for i in ( 30 | line_regex.match(l) 31 | for l 32 | in target.run_command("network") 33 | ) if i is not None 34 | ) 35 | ) 36 | keys = fields[0] 37 | values = fields[1:] 38 | return dict((fields[0], dict(zip(keys, ((v if v != "-" else None) for v in fields)))) for fields in values) 39 | 40 | def get_fan_speed(target): 41 | line_regex = re.compile(r'^FanSpeed:(.+)$') 42 | field_regex = re.compile(r'([A-Z][a-z]+) ([0-9]+) \[rpm\]') 43 | 44 | fields = \ 45 | list( 46 | dict( 47 | (i.group(1), int(i.group(2))) for i in field_regex.finditer(l) 48 | ) 49 | for l 50 | in ( 51 | i.group(1) for i in ( 52 | line_regex.match(l) 53 | for l 54 | in target.run_command("fanspeed") 55 | ) if i is not None 56 | ) 57 | ) 58 | return merge(fields) 59 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "duco" : { 3 | "type": "serial", 4 | "device": "/dev/serial0", 5 | "baudrate": 115200, 6 | "control": "gpio" 7 | }, 8 | "control": { 9 | "type": "gpio", 10 | "active_low": true, 11 | "states": { 12 | "CNT1" : { 13 | "17": 0, 14 | "27": 0 15 | }, 16 | "CNT2" : { 17 | "17": 1, 18 | "27": 0 19 | }, 20 | "CNT3" : { 21 | "17": 0, 22 | "27": 1 23 | } 24 | } 25 | }, 26 | "mqtt" : { 27 | "client_id": "duco", 28 | "host": "127.0.0.1", 29 | "port": 1883, 30 | "keepalive": 60, 31 | "bind_address": "", 32 | "username": null, 33 | "password": null, 34 | "qos": 0, 35 | "pub_topic_namespace": "value/duco", 36 | "sub_topic_namespace": "set/duco", 37 | "retain": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /control.py: -------------------------------------------------------------------------------- 1 | 2 | class DucoboxControl(object): 3 | 4 | def __init__(self, config): 5 | pass 6 | 7 | # TODO: should probably have open and close methods 8 | 9 | def open(self): 10 | raise NotImplementedError("Abstract method") 11 | 12 | def close(self): 13 | raise NotImplementedError("Abstract method") 14 | 15 | def set_state(self, value): 16 | raise NotImplementedError("Abstract method") 17 | -------------------------------------------------------------------------------- /control_gpio.py: -------------------------------------------------------------------------------- 1 | from control import DucoboxControl 2 | import RPi.GPIO as GPIO 3 | import logging 4 | from time import sleep 5 | # Set up logging 6 | logging.basicConfig(level=logging.INFO) 7 | log = logging.getLogger(__name__) 8 | 9 | class DucoboxGpioControl(DucoboxControl): 10 | 11 | def __init__(self, config): 12 | log.info("Initializing Duco GPIO controller") 13 | super(DucoboxGpioControl, self).__init__(config) 14 | self._invert = config['active_low'] 15 | self._states = dict( 16 | (state.upper(), dict( 17 | ( 18 | int(pin), 19 | val != self._invert 20 | ) for 21 | (pin, val) in sett.items()) 22 | ) for 23 | (state, sett) 24 | in config['states'].items()) 25 | # this gets the unique pin numbers from the states object, 26 | # I have no idea how it works, but it does 27 | self._pins = set(k for v in self._states.values() for k in v.keys()) 28 | self._values = {} 29 | 30 | def open(self): 31 | GPIO.setmode(GPIO.BCM) 32 | initial_value = GPIO.HIGH if self._invert else GPIO.LOW 33 | for pin in self._pins: 34 | GPIO.setup(pin, GPIO.OUT, initial=initial_value) 35 | self._initial_values = dict((pin, initial_value) for pin in self._pins) 36 | self._values = dict(self._initial_values) 37 | 38 | def close(self): 39 | GPIO.cleanup() 40 | 41 | def set_state(self, value): 42 | value = value.upper() 43 | if value not in self._states: 44 | raise NotImplementedError("Unsupported value {}".format(value)) 45 | self.set_values(self._states[value]) 46 | 47 | def set_values(self, values): 48 | current_values = dict((p,v) for (p,v) 49 | in self._values.iteritems() 50 | if p in values) 51 | 52 | if current_values == values: 53 | if values == self._initial_values: 54 | self.set_state("CNT2") 55 | else: 56 | self.set_values(self._initial_values) 57 | sleep(0.1) 58 | 59 | for (pin, pin_value) in values.iteritems(): 60 | GPIO.output(pin, pin_value) 61 | self._values[pin] = pin_value 62 | -------------------------------------------------------------------------------- /duco.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import re 3 | import json 4 | import sys 5 | import logging 6 | import paho.mqtt.client as mqtt 7 | from datetime import datetime 8 | from time import sleep 9 | from threading import Thread 10 | 11 | from utils import pathify, changes 12 | # TODO: clean this up 13 | from commands import * 14 | 15 | # Set up logging 16 | logging.basicConfig(level=logging.INFO) 17 | log = logging.getLogger(__name__) 18 | 19 | _running = None 20 | settings = None 21 | mqtt_client = None 22 | conn = None 23 | ctrl = None 24 | topic_namespace = None 25 | initial_get = True 26 | 27 | def init(): 28 | global _running, settings, mqtt_client, conn, topic_namespace, ctrl 29 | # Default settings 30 | settings = { 31 | "duco" : { 32 | "type": "serial", 33 | "device": "/dev/serial0", 34 | "baudrate": 115200 35 | }, 36 | "control": { 37 | "type": "gpio", 38 | "active_low": True, 39 | "states": {} 40 | }, 41 | "mqtt" : { 42 | "client_id": "duco", 43 | "host": "127.0.0.1", 44 | "port": 1883, 45 | "keepalive": 60, 46 | "bind_address": "", 47 | "username": None, 48 | "password": None, 49 | "qos": 0, 50 | "pub_topic_namespace": "value/duco", 51 | "sub_topic_namespace": "set/duco", 52 | "retain": False 53 | } 54 | } 55 | # Update default settings from the settings file 56 | with open('config.json') as f: 57 | settings.update(json.load(f)) 58 | 59 | # Set the namespace of the mqtt messages from the settings 60 | topic_namespace=settings['mqtt']['pub_topic_namespace'] 61 | 62 | log.info("Initializing MQTT") 63 | 64 | # Set up paho-mqtt 65 | mqtt_client = mqtt.Client( 66 | client_id=settings['mqtt']['client_id']) 67 | mqtt_client.on_connect = on_mqtt_connect 68 | mqtt_client.on_message = on_mqtt_message 69 | 70 | if settings['mqtt']['username']: 71 | mqtt_client.username_pw_set( 72 | settings['mqtt']['username'], 73 | settings['mqtt']['password']) 74 | 75 | # The will makes sure the device registers as offline when the connection 76 | # is lost 77 | mqtt_client.will_set( 78 | topic=topic_namespace, 79 | payload="offline", 80 | qos=settings['mqtt']['qos'], 81 | retain=True) 82 | 83 | # Let's not wait for the connection, as it may not succeed if we're not 84 | # connected to the network or anything. Such is the beauty of MQTT 85 | mqtt_client.connect_async( 86 | host=settings['mqtt']['host'], 87 | port=settings['mqtt']['port'], 88 | keepalive=settings['mqtt']['keepalive'], 89 | bind_address=settings['mqtt']['bind_address']) 90 | mqtt_client.loop_start() 91 | 92 | log.info("Initializing Duco") 93 | 94 | duco_type = { 95 | "serial" : lambda: __import__('ducobox_serial', 96 | globals(), locals(), ['DucoboxSerialClient'], 0) \ 97 | .DucoboxSerialClient, 98 | }[settings['duco']['type']]() 99 | 100 | conn = duco_type(settings['duco']) 101 | 102 | 103 | control_types = { 104 | "gpio" : lambda: __import__('control_gpio', 105 | globals(), locals(), ['DucoboxGpioControl'], 0) \ 106 | .DucoboxGpioControl, 107 | } 108 | my_type = settings['control']['type'] 109 | if my_type in control_types: 110 | control_type = control_types[my_type]() 111 | ctrl = control_type(settings['control']) 112 | 113 | _running = True 114 | 115 | 116 | def main(): 117 | init() 118 | # TODO: bleeegh 119 | global _running 120 | 121 | conn.open() 122 | if ctrl != None: 123 | ctrl.open() 124 | 125 | _worker = Thread(target=worker) 126 | _worker.start() 127 | try: 128 | while _running: 129 | sleep(60) 130 | except: 131 | log.warn("Handling exception") 132 | _running = False 133 | _worker.join() 134 | 135 | conn.close() 136 | if ctrl != None: 137 | ctrl.close() 138 | 139 | 140 | def publish(topic, payload): 141 | mqtt_client.publish( 142 | topic=topic, 143 | payload=payload, 144 | qos=settings['mqtt']['qos'], 145 | retain=settings['mqtt']['retain']) 146 | 147 | 148 | def worker(): 149 | # TODO: clean this up 150 | global _running, initial_get 151 | _running = True 152 | initial_get = True 153 | 154 | log.info("Starting read loop") 155 | while _running: 156 | if initial_get: 157 | initial_get = False 158 | netw = {} 159 | fan = {} 160 | co2 = None 161 | humi = None 162 | temp = None 163 | 164 | try: 165 | _netw = get_network_data(conn) 166 | cnetw = changes(netw, _netw) 167 | netw = _netw 168 | for (t, v) in pathify(cnetw, "{}/network".format(topic_namespace)): 169 | publish(t, v) 170 | except Exception as e: 171 | log.warn("An exception occured getting network, skipping", exc_info=True) 172 | 173 | try: 174 | _fan = get_fan_speed(conn) 175 | cfan = changes(fan, _fan) 176 | fan = _fan 177 | for (t, v) in pathify(cfan, "{}/fan".format(topic_namespace)): 178 | publish(t, v) 179 | except Exception as e: 180 | log.warn("An exception occured getting fan speed, skipping", exc_info=True) 181 | 182 | try: 183 | _temp = get_temperature(conn) 184 | if _temp != temp: 185 | temp = _temp 186 | publish("{}/temp".format(topic_namespace), temp) 187 | except Exception as e: 188 | log.warn("An exception occured getting temp, skipping", exc_info=True) 189 | 190 | try: 191 | _co2 = get_co2(conn) 192 | if _co2 != co2: 193 | co2 = _co2 194 | publish("{}/co2".format(topic_namespace), co2) 195 | except Exception as e: 196 | log.warn("An exception occured getting CO2, skipping", exc_info=True) 197 | 198 | try: 199 | _humi = get_humidity(conn) 200 | if _humi != humi: 201 | humi = _humi 202 | publish("{}/humi".format(topic_namespace), humi) 203 | except Exception as e: 204 | log.warn("An exception occured getting humidity, skipping", exc_info=True) 205 | 206 | for _ in range(15): 207 | sleep(1) 208 | if not _running: 209 | break 210 | 211 | 212 | def get_all(_): 213 | global initial_get 214 | initial_get = True 215 | 216 | def on_mqtt_connect(client, userdata, flags, rc): 217 | # Subscribe to all topics in our namespace when we're connected. Send out 218 | # a message telling we're online 219 | log.info("Connected with result code "+str(rc)) 220 | mqtt_client.subscribe('{}/#'.format(settings['mqtt']['sub_topic_namespace'])) 221 | mqtt_client.subscribe('{}'.format(settings['mqtt']['sub_topic_namespace'])) 222 | mqtt_client.publish( 223 | topic=topic_namespace, 224 | payload="online", 225 | qos=settings['mqtt']['qos'], 226 | retain=settings['mqtt']['retain']) 227 | 228 | def on_mqtt_message(client, userdata, msg): 229 | # Handle incoming messages 230 | log.info("Received message on topic {} with payload {}".format( 231 | msg.topic, str(msg.payload))) 232 | namespace = settings['mqtt']['sub_topic_namespace'] 233 | command_generators={ 234 | "{}/state".format(namespace): \ 235 | lambda _ :ctrl.set_state(_), 236 | "{}/get".format(namespace): \ 237 | get_all, 238 | } 239 | # Find the correct command generator from the dict above 240 | command = command_generators.get(msg.topic) 241 | if command: 242 | log.debug("Calling command") 243 | # Get the command and call it 244 | command(msg.payload) 245 | -------------------------------------------------------------------------------- /ducobox.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import re 3 | import logging 4 | 5 | log = logging.getLogger(__name__) 6 | 7 | 8 | class DucoboxClient(object): 9 | 10 | def __init__(self, config): 11 | #TODO: something 12 | self.fail_retry_wait = 0.25 13 | self.read_timeout = 0.1 14 | 15 | def open(self): 16 | raise NotImplementedError("Abstract method") 17 | 18 | def close(self): 19 | raise NotImplementedError("Abstract method") 20 | 21 | def write(self, data): 22 | raise NotImplementedError("Abstract method") 23 | 24 | 25 | def read(self, timeout): 26 | raise NotImplementedError("Abstract method") 27 | 28 | 29 | def run_command(self, data, timeout=1, retry=5): 30 | """ 31 | Execute a command and get the response 32 | 33 | retry if it fails 34 | """ 35 | try: 36 | return self._run_command(data, timeout) 37 | except RuntimeError: 38 | if retry > 0: 39 | sleep(self.fail_retry_wait) 40 | return self.run_command(data, timeout, retry-1) 41 | raise 42 | 43 | 44 | def _run_command(self, data, timeout): 45 | """ 46 | Execute a command and get the response 47 | 48 | raise an exception if it fails 49 | """ 50 | self.write(data + "\r") 51 | result = list(self._read_response_lines(timeout)) 52 | if data.strip() != result[0] or result[-1] != "" or \ 53 | result[2] == "[WRN] CommDllPsiRf.c - line 516 : RX++" or \ 54 | result[2] == "Failed": 55 | raise RuntimeError("Unexpected response") 56 | return result 57 | 58 | def _read_response_lines(self, timeout=1): 59 | line_splitter = re.compile(r'^(.*?)[\r\n]+') 60 | 61 | # Create a buffer for read data 62 | data = "" 63 | c_count = 0 64 | m_count = timeout / self.read_timeout 65 | while True: 66 | # Call the read method of the implementation 67 | n_data = self.read(self.read_timeout) 68 | if n_data == "": 69 | c_count += 1 70 | if c_count > m_count: 71 | raise RuntimeError("Timeout in read") 72 | else: 73 | c_count = 0 74 | data += n_data 75 | if data == "> ": 76 | return 77 | # Find all the lines in the read data 78 | while True: 79 | m = line_splitter.match(data) 80 | if not m: 81 | # There are no full lines yet, so we have to read some more 82 | break 83 | data = data[m.end():] 84 | line = m.group(1).strip() 85 | yield line 86 | -------------------------------------------------------------------------------- /ducobox_serial.py: -------------------------------------------------------------------------------- 1 | from ducobox import DucoboxClient 2 | from serial import Serial 3 | from time import sleep 4 | import logging 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | 9 | class DucoboxSerialClient(DucoboxClient): 10 | 11 | def __init__(self, config): 12 | log.info("Initializing Duco serial client") 13 | super(DucoboxSerialClient, self).__init__(config) 14 | self._device = config["device"] 15 | self._baudrate = config["baudrate"] 16 | self.inter_char_delay = 0.001 17 | self.c = None 18 | 19 | def open(self): 20 | self.c = Serial( 21 | port=self._device, 22 | baudrate=self._baudrate, 23 | timeout=0.1) 24 | 25 | def close(self): 26 | self.c.close() 27 | 28 | def write(self, data): 29 | for d in data: 30 | self.c.write(d) 31 | self.c.flush() 32 | sleep(self.inter_char_delay) 33 | 34 | 35 | def read(self, timeout): 36 | if self.c.timeout != timeout: 37 | self.c.timeout = timeout 38 | return self.c.read(128) 39 | -------------------------------------------------------------------------------- /paho/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martenjacobs/py-duco-mqtt/6640015a8519a6936951c3f6b4f8cbd2c18ba820/paho/__init__.py -------------------------------------------------------------------------------- /paho/mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.3.1" 2 | 3 | 4 | class MQTTException(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /paho/mqtt/client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012-2014 Roger Light 2 | # 3 | # All rights reserved. This program and the accompanying materials 4 | # are made available under the terms of the Eclipse Public License v1.0 5 | # and Eclipse Distribution License v1.0 which accompany this distribution. 6 | # 7 | # The Eclipse Public License is available at 8 | # http://www.eclipse.org/legal/epl-v10.html 9 | # and the Eclipse Distribution License is available at 10 | # http://www.eclipse.org/org/documents/edl-v10.php. 11 | # 12 | # Contributors: 13 | # Roger Light - initial API and implementation 14 | 15 | """ 16 | This is an MQTT v3.1 client module. MQTT is a lightweight pub/sub messaging 17 | protocol that is easy to implement and suitable for low powered devices. 18 | """ 19 | import collections 20 | import errno 21 | import platform 22 | import random 23 | import select 24 | import socket 25 | 26 | try: 27 | import ssl 28 | except ImportError: 29 | ssl = None 30 | 31 | import struct 32 | import sys 33 | import threading 34 | 35 | import time 36 | import uuid 37 | import base64 38 | import string 39 | import hashlib 40 | import logging 41 | 42 | try: 43 | # Use monotonic clock if available 44 | time_func = time.monotonic 45 | except AttributeError: 46 | time_func = time.time 47 | 48 | try: 49 | import dns.resolver 50 | except ImportError: 51 | HAVE_DNS = False 52 | else: 53 | HAVE_DNS = True 54 | 55 | from .matcher import MQTTMatcher 56 | 57 | if platform.system() == 'Windows': 58 | EAGAIN = errno.WSAEWOULDBLOCK 59 | else: 60 | EAGAIN = errno.EAGAIN 61 | 62 | MQTTv31 = 3 63 | MQTTv311 = 4 64 | 65 | if sys.version_info[0] >= 3: 66 | # define some alias for python2 compatibility 67 | unicode = str 68 | basestring = str 69 | 70 | # Message types 71 | CONNECT = 0x10 72 | CONNACK = 0x20 73 | PUBLISH = 0x30 74 | PUBACK = 0x40 75 | PUBREC = 0x50 76 | PUBREL = 0x60 77 | PUBCOMP = 0x70 78 | SUBSCRIBE = 0x80 79 | SUBACK = 0x90 80 | UNSUBSCRIBE = 0xA0 81 | UNSUBACK = 0xB0 82 | PINGREQ = 0xC0 83 | PINGRESP = 0xD0 84 | DISCONNECT = 0xE0 85 | 86 | # Log levels 87 | MQTT_LOG_INFO = 0x01 88 | MQTT_LOG_NOTICE = 0x02 89 | MQTT_LOG_WARNING = 0x04 90 | MQTT_LOG_ERR = 0x08 91 | MQTT_LOG_DEBUG = 0x10 92 | LOGGING_LEVEL = { 93 | MQTT_LOG_DEBUG: logging.DEBUG, 94 | MQTT_LOG_INFO: logging.INFO, 95 | MQTT_LOG_NOTICE: logging.INFO, # This has no direct equivalent level 96 | MQTT_LOG_WARNING: logging.WARNING, 97 | MQTT_LOG_ERR: logging.ERROR, 98 | } 99 | 100 | # CONNACK codes 101 | CONNACK_ACCEPTED = 0 102 | CONNACK_REFUSED_PROTOCOL_VERSION = 1 103 | CONNACK_REFUSED_IDENTIFIER_REJECTED = 2 104 | CONNACK_REFUSED_SERVER_UNAVAILABLE = 3 105 | CONNACK_REFUSED_BAD_USERNAME_PASSWORD = 4 106 | CONNACK_REFUSED_NOT_AUTHORIZED = 5 107 | 108 | # Connection state 109 | mqtt_cs_new = 0 110 | mqtt_cs_connected = 1 111 | mqtt_cs_disconnecting = 2 112 | mqtt_cs_connect_async = 3 113 | 114 | # Message state 115 | mqtt_ms_invalid = 0 116 | mqtt_ms_publish = 1 117 | mqtt_ms_wait_for_puback = 2 118 | mqtt_ms_wait_for_pubrec = 3 119 | mqtt_ms_resend_pubrel = 4 120 | mqtt_ms_wait_for_pubrel = 5 121 | mqtt_ms_resend_pubcomp = 6 122 | mqtt_ms_wait_for_pubcomp = 7 123 | mqtt_ms_send_pubrec = 8 124 | mqtt_ms_queued = 9 125 | 126 | # Error values 127 | MQTT_ERR_AGAIN = -1 128 | MQTT_ERR_SUCCESS = 0 129 | MQTT_ERR_NOMEM = 1 130 | MQTT_ERR_PROTOCOL = 2 131 | MQTT_ERR_INVAL = 3 132 | MQTT_ERR_NO_CONN = 4 133 | MQTT_ERR_CONN_REFUSED = 5 134 | MQTT_ERR_NOT_FOUND = 6 135 | MQTT_ERR_CONN_LOST = 7 136 | MQTT_ERR_TLS = 8 137 | MQTT_ERR_PAYLOAD_SIZE = 9 138 | MQTT_ERR_NOT_SUPPORTED = 10 139 | MQTT_ERR_AUTH = 11 140 | MQTT_ERR_ACL_DENIED = 12 141 | MQTT_ERR_UNKNOWN = 13 142 | MQTT_ERR_ERRNO = 14 143 | MQTT_ERR_QUEUE_SIZE = 15 144 | 145 | sockpair_data = b"0" 146 | 147 | 148 | class WebsocketConnectionError(ValueError): 149 | pass 150 | 151 | 152 | def error_string(mqtt_errno): 153 | """Return the error string associated with an mqtt error number.""" 154 | if mqtt_errno == MQTT_ERR_SUCCESS: 155 | return "No error." 156 | elif mqtt_errno == MQTT_ERR_NOMEM: 157 | return "Out of memory." 158 | elif mqtt_errno == MQTT_ERR_PROTOCOL: 159 | return "A network protocol error occurred when communicating with the broker." 160 | elif mqtt_errno == MQTT_ERR_INVAL: 161 | return "Invalid function arguments provided." 162 | elif mqtt_errno == MQTT_ERR_NO_CONN: 163 | return "The client is not currently connected." 164 | elif mqtt_errno == MQTT_ERR_CONN_REFUSED: 165 | return "The connection was refused." 166 | elif mqtt_errno == MQTT_ERR_NOT_FOUND: 167 | return "Message not found (internal error)." 168 | elif mqtt_errno == MQTT_ERR_CONN_LOST: 169 | return "The connection was lost." 170 | elif mqtt_errno == MQTT_ERR_TLS: 171 | return "A TLS error occurred." 172 | elif mqtt_errno == MQTT_ERR_PAYLOAD_SIZE: 173 | return "Payload too large." 174 | elif mqtt_errno == MQTT_ERR_NOT_SUPPORTED: 175 | return "This feature is not supported." 176 | elif mqtt_errno == MQTT_ERR_AUTH: 177 | return "Authorisation failed." 178 | elif mqtt_errno == MQTT_ERR_ACL_DENIED: 179 | return "Access denied by ACL." 180 | elif mqtt_errno == MQTT_ERR_UNKNOWN: 181 | return "Unknown error." 182 | elif mqtt_errno == MQTT_ERR_ERRNO: 183 | return "Error defined by errno." 184 | else: 185 | return "Unknown error." 186 | 187 | 188 | def connack_string(connack_code): 189 | """Return the string associated with a CONNACK result.""" 190 | if connack_code == CONNACK_ACCEPTED: 191 | return "Connection Accepted." 192 | elif connack_code == CONNACK_REFUSED_PROTOCOL_VERSION: 193 | return "Connection Refused: unacceptable protocol version." 194 | elif connack_code == CONNACK_REFUSED_IDENTIFIER_REJECTED: 195 | return "Connection Refused: identifier rejected." 196 | elif connack_code == CONNACK_REFUSED_SERVER_UNAVAILABLE: 197 | return "Connection Refused: broker unavailable." 198 | elif connack_code == CONNACK_REFUSED_BAD_USERNAME_PASSWORD: 199 | return "Connection Refused: bad user name or password." 200 | elif connack_code == CONNACK_REFUSED_NOT_AUTHORIZED: 201 | return "Connection Refused: not authorised." 202 | else: 203 | return "Connection Refused: unknown reason." 204 | 205 | 206 | def base62(num, base=string.digits + string.ascii_letters, padding=1): 207 | """Convert a number to base-62 representation.""" 208 | assert num >= 0 209 | digits = [] 210 | while num: 211 | num, rest = divmod(num, 62) 212 | digits.append(base[rest]) 213 | digits.extend(base[0] for _ in range(len(digits), padding)) 214 | return ''.join(reversed(digits)) 215 | 216 | 217 | def topic_matches_sub(sub, topic): 218 | """Check whether a topic matches a subscription. 219 | 220 | For example: 221 | 222 | foo/bar would match the subscription foo/# or +/bar 223 | non/matching would not match the subscription non/+/+ 224 | """ 225 | matcher = MQTTMatcher() 226 | matcher[sub] = True 227 | try: 228 | next(matcher.iter_match(topic)) 229 | return True 230 | except StopIteration: 231 | return False 232 | 233 | 234 | def _socketpair_compat(): 235 | """TCP/IP socketpair including Windows support""" 236 | listensock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP) 237 | listensock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 238 | listensock.bind(("127.0.0.1", 0)) 239 | listensock.listen(1) 240 | 241 | iface, port = listensock.getsockname() 242 | sock1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP) 243 | sock1.setblocking(0) 244 | try: 245 | sock1.connect(("127.0.0.1", port)) 246 | except socket.error as err: 247 | if err.errno != errno.EINPROGRESS and err.errno != errno.EWOULDBLOCK and err.errno != EAGAIN: 248 | raise 249 | sock2, address = listensock.accept() 250 | sock2.setblocking(0) 251 | listensock.close() 252 | return (sock1, sock2) 253 | 254 | 255 | class MQTTMessageInfo(object): 256 | """This is a class returned from Client.publish() and can be used to find 257 | out the mid of the message that was published, and to determine whether the 258 | message has been published, and/or wait until it is published. 259 | """ 260 | 261 | __slots__ = 'mid', '_published', '_condition', 'rc', '_iterpos' 262 | 263 | def __init__(self, mid): 264 | self.mid = mid 265 | self._published = False 266 | self._condition = threading.Condition() 267 | self.rc = 0 268 | self._iterpos = 0 269 | 270 | def __str__(self): 271 | return str((self.rc, self.mid)) 272 | 273 | def __iter__(self): 274 | self._iterpos = 0 275 | return self 276 | 277 | def __next__(self): 278 | return self.next() 279 | 280 | def next(self): 281 | if self._iterpos == 0: 282 | self._iterpos = 1 283 | return self.rc 284 | elif self._iterpos == 1: 285 | self._iterpos = 2 286 | return self.mid 287 | else: 288 | raise StopIteration 289 | 290 | def __getitem__(self, index): 291 | if index == 0: 292 | return self.rc 293 | elif index == 1: 294 | return self.mid 295 | else: 296 | raise IndexError("index out of range") 297 | 298 | def _set_as_published(self): 299 | with self._condition: 300 | self._published = True 301 | self._condition.notify() 302 | 303 | def wait_for_publish(self): 304 | """Block until the message associated with this object is published.""" 305 | if self.rc == MQTT_ERR_QUEUE_SIZE: 306 | raise ValueError('Message is not queued due to ERR_QUEUE_SIZE') 307 | with self._condition: 308 | while not self._published: 309 | self._condition.wait() 310 | 311 | def is_published(self): 312 | """Returns True if the message associated with this object has been 313 | published, else returns False.""" 314 | if self.rc == MQTT_ERR_QUEUE_SIZE: 315 | raise ValueError('Message is not queued due to ERR_QUEUE_SIZE') 316 | with self._condition: 317 | return self._published 318 | 319 | 320 | class MQTTMessage(object): 321 | """ This is a class that describes an incoming or outgoing message. It is 322 | passed to the on_message callback as the message parameter. 323 | 324 | Members: 325 | 326 | topic : String/bytes. topic that the message was published on. 327 | payload : String/bytes the message payload. 328 | qos : Integer. The message Quality of Service 0, 1 or 2. 329 | retain : Boolean. If true, the message is a retained message and not fresh. 330 | mid : Integer. The message id. 331 | 332 | On Python 3, topic must be bytes. 333 | """ 334 | 335 | __slots__ = 'timestamp', 'state', 'dup', 'mid', '_topic', 'payload', 'qos', 'retain', 'info' 336 | 337 | def __init__(self, mid=0, topic=b""): 338 | self.timestamp = 0 339 | self.state = mqtt_ms_invalid 340 | self.dup = False 341 | self.mid = mid 342 | self._topic = topic 343 | self.payload = b"" 344 | self.qos = 0 345 | self.retain = False 346 | self.info = MQTTMessageInfo(mid) 347 | 348 | def __eq__(self, other): 349 | """Override the default Equals behavior""" 350 | if isinstance(other, self.__class__): 351 | return self.mid == other.mid 352 | return False 353 | 354 | def __ne__(self, other): 355 | """Define a non-equality test""" 356 | return not self.__eq__(other) 357 | 358 | @property 359 | def topic(self): 360 | return self._topic.decode('utf-8') 361 | 362 | @topic.setter 363 | def topic(self, value): 364 | self._topic = value 365 | 366 | 367 | class Client(object): 368 | """MQTT version 3.1/3.1.1 client class. 369 | 370 | This is the main class for use communicating with an MQTT broker. 371 | 372 | General usage flow: 373 | 374 | * Use connect()/connect_async() to connect to a broker 375 | * Call loop() frequently to maintain network traffic flow with the broker 376 | * Or use loop_start() to set a thread running to call loop() for you. 377 | * Or use loop_forever() to handle calling loop() for you in a blocking 378 | * function. 379 | * Use subscribe() to subscribe to a topic and receive messages 380 | * Use publish() to send messages 381 | * Use disconnect() to disconnect from the broker 382 | 383 | Data returned from the broker is made available with the use of callback 384 | functions as described below. 385 | 386 | Callbacks 387 | ========= 388 | 389 | A number of callback functions are available to receive data back from the 390 | broker. To use a callback, define a function and then assign it to the 391 | client: 392 | 393 | def on_connect(client, userdata, flags, rc): 394 | print("Connection returned " + str(rc)) 395 | 396 | client.on_connect = on_connect 397 | 398 | All of the callbacks as described below have a "client" and an "userdata" 399 | argument. "client" is the Client instance that is calling the callback. 400 | "userdata" is user data of any type and can be set when creating a new client 401 | instance or with user_data_set(userdata). 402 | 403 | The callbacks: 404 | 405 | on_connect(client, userdata, flags, rc): called when the broker responds to our connection 406 | request. 407 | flags is a dict that contains response flags from the broker: 408 | flags['session present'] - this flag is useful for clients that are 409 | using clean session set to 0 only. If a client with clean 410 | session=0, that reconnects to a broker that it has previously 411 | connected to, this flag indicates whether the broker still has the 412 | session information for the client. If 1, the session still exists. 413 | The value of rc determines success or not: 414 | 0: Connection successful 415 | 1: Connection refused - incorrect protocol version 416 | 2: Connection refused - invalid client identifier 417 | 3: Connection refused - server unavailable 418 | 4: Connection refused - bad username or password 419 | 5: Connection refused - not authorised 420 | 6-255: Currently unused. 421 | 422 | on_disconnect(client, userdata, rc): called when the client disconnects from the broker. 423 | The rc parameter indicates the disconnection state. If MQTT_ERR_SUCCESS 424 | (0), the callback was called in response to a disconnect() call. If any 425 | other value the disconnection was unexpected, such as might be caused by 426 | a network error. 427 | 428 | on_message(client, userdata, message): called when a message has been received on a 429 | topic that the client subscribes to. The message variable is a 430 | MQTTMessage that describes all of the message parameters. 431 | 432 | on_publish(client, userdata, mid): called when a message that was to be sent using the 433 | publish() call has completed transmission to the broker. For messages 434 | with QoS levels 1 and 2, this means that the appropriate handshakes have 435 | completed. For QoS 0, this simply means that the message has left the 436 | client. The mid variable matches the mid variable returned from the 437 | corresponding publish() call, to allow outgoing messages to be tracked. 438 | This callback is important because even if the publish() call returns 439 | success, it does not always mean that the message has been sent. 440 | 441 | on_subscribe(client, userdata, mid, granted_qos): called when the broker responds to a 442 | subscribe request. The mid variable matches the mid variable returned 443 | from the corresponding subscribe() call. The granted_qos variable is a 444 | list of integers that give the QoS level the broker has granted for each 445 | of the different subscription requests. 446 | 447 | on_unsubscribe(client, userdata, mid): called when the broker responds to an unsubscribe 448 | request. The mid variable matches the mid variable returned from the 449 | corresponding unsubscribe() call. 450 | 451 | on_log(client, userdata, level, buf): called when the client has log information. Define 452 | to allow debugging. The level variable gives the severity of the message 453 | and will be one of MQTT_LOG_INFO, MQTT_LOG_NOTICE, MQTT_LOG_WARNING, 454 | MQTT_LOG_ERR, and MQTT_LOG_DEBUG. The message itself is in buf. 455 | 456 | """ 457 | 458 | def __init__(self, client_id="", clean_session=True, userdata=None, 459 | protocol=MQTTv311, transport="tcp"): 460 | """client_id is the unique client id string used when connecting to the 461 | broker. If client_id is zero length or None, then the behaviour is 462 | defined by which protocol version is in use. If using MQTT v3.1.1, then 463 | a zero length client id will be sent to the broker and the broker will 464 | generate a random for the client. If using MQTT v3.1 then an id will be 465 | randomly generated. In both cases, clean_session must be True. If this 466 | is not the case a ValueError will be raised. 467 | 468 | clean_session is a boolean that determines the client type. If True, 469 | the broker will remove all information about this client when it 470 | disconnects. If False, the client is a persistent client and 471 | subscription information and queued messages will be retained when the 472 | client disconnects. 473 | Note that a client will never discard its own outgoing messages on 474 | disconnect. Calling connect() or reconnect() will cause the messages to 475 | be resent. Use reinitialise() to reset a client to its original state. 476 | 477 | userdata is user defined data of any type that is passed as the "userdata" 478 | parameter to callbacks. It may be updated at a later point with the 479 | user_data_set() function. 480 | 481 | The protocol argument allows explicit setting of the MQTT version to 482 | use for this client. Can be paho.mqtt.client.MQTTv311 (v3.1.1) or 483 | paho.mqtt.client.MQTTv31 (v3.1), with the default being v3.1.1 If the 484 | broker reports that the client connected with an invalid protocol 485 | version, the client will automatically attempt to reconnect using v3.1 486 | instead. 487 | 488 | Set transport to "websockets" to use WebSockets as the transport 489 | mechanism. Set to "tcp" to use raw TCP, which is the default. 490 | """ 491 | if not clean_session and (client_id == "" or client_id is None): 492 | raise ValueError('A client id must be provided if clean session is False.') 493 | 494 | self._transport = transport 495 | self._protocol = protocol 496 | self._userdata = userdata 497 | self._sock = None 498 | self._sockpairR, self._sockpairW = _socketpair_compat() 499 | self._keepalive = 60 500 | self._message_retry = 20 501 | self._last_retry_check = 0 502 | self._clean_session = clean_session 503 | 504 | # [MQTT-3.1.3-4] Client Id must be UTF-8 encoded string. 505 | if client_id == "" or client_id is None: 506 | if protocol == MQTTv31: 507 | self._client_id = base62(uuid.uuid4().int, padding=22) 508 | else: 509 | self._client_id = b"" 510 | else: 511 | self._client_id = client_id 512 | if isinstance(self._client_id, unicode): 513 | self._client_id = self._client_id.encode('utf-8') 514 | 515 | self._username = None 516 | self._password = None 517 | self._in_packet = { 518 | "command": 0, 519 | "have_remaining": 0, 520 | "remaining_count": [], 521 | "remaining_mult": 1, 522 | "remaining_length": 0, 523 | "packet": b"", 524 | "to_process": 0, 525 | "pos": 0} 526 | self._out_packet = collections.deque() 527 | self._current_out_packet = None 528 | self._last_msg_in = time_func() 529 | self._last_msg_out = time_func() 530 | self._reconnect_min_delay = 1 531 | self._reconnect_max_delay = 120 532 | self._reconnect_delay = None 533 | self._ping_t = 0 534 | self._last_mid = 0 535 | self._state = mqtt_cs_new 536 | self._out_messages = [] 537 | self._in_messages = [] 538 | self._max_inflight_messages = 20 539 | self._inflight_messages = 0 540 | self._max_queued_messages = 0 541 | self._will = False 542 | self._will_topic = b"" 543 | self._will_payload = b"" 544 | self._will_qos = 0 545 | self._will_retain = False 546 | self._on_message_filtered = MQTTMatcher() 547 | self._host = "" 548 | self._port = 1883 549 | self._bind_address = "" 550 | self._in_callback = threading.Lock() 551 | self._callback_mutex = threading.RLock() 552 | self._out_packet_mutex = threading.Lock() 553 | self._current_out_packet_mutex = threading.RLock() 554 | self._msgtime_mutex = threading.Lock() 555 | self._out_message_mutex = threading.RLock() 556 | self._in_message_mutex = threading.Lock() 557 | self._reconnect_delay_mutex = threading.Lock() 558 | self._thread = None 559 | self._thread_terminate = False 560 | self._ssl = False 561 | self._ssl_context = None 562 | self._tls_insecure = False # Only used when SSL context does not have check_hostname attribute 563 | self._logger = None 564 | # No default callbacks 565 | self._on_log = None 566 | self._on_connect = None 567 | self._on_subscribe = None 568 | self._on_message = None 569 | self._on_publish = None 570 | self._on_unsubscribe = None 571 | self._on_disconnect = None 572 | self._websocket_path = "/mqtt" 573 | self._websocket_extra_headers = None 574 | 575 | def __del__(self): 576 | pass 577 | 578 | def reinitialise(self, client_id="", clean_session=True, userdata=None): 579 | if self._sock: 580 | self._sock.close() 581 | self._sock = None 582 | if self._sockpairR: 583 | self._sockpairR.close() 584 | self._sockpairR = None 585 | if self._sockpairW: 586 | self._sockpairW.close() 587 | self._sockpairW = None 588 | 589 | self.__init__(client_id, clean_session, userdata) 590 | 591 | def ws_set_options(self, path="/mqtt", headers=None): 592 | """ Set the path and headers for a websocket connection 593 | 594 | path is a string starting with / which should be the endpoint of the 595 | mqtt connection on the remote server 596 | 597 | headers can be either a dict or a callable object. If it is a dict then 598 | the extra items in the dict are added to the websocket headers. If it is 599 | a callable, then the default websocket headers are passed into this 600 | function and the result is used as the new headers. 601 | """ 602 | self._websocket_path = path 603 | 604 | if headers is not None: 605 | if isinstance(headers, dict) or callable(headers): 606 | self._websocket_extra_headers = headers 607 | else: 608 | raise ValueError("'headers' option to ws_set_options has to be either a dictionary or callable") 609 | 610 | def tls_set_context(self, context=None): 611 | """Configure network encryption and authentication context. Enables SSL/TLS support. 612 | 613 | context : an ssl.SSLContext object. By default this is given by 614 | `ssl.create_default_context()`, if available. 615 | 616 | Must be called before connect() or connect_async().""" 617 | if self._ssl_context is not None: 618 | raise ValueError('SSL/TLS has already been configured.') 619 | 620 | # Assume that have SSL support, or at least that context input behaves like ssl.SSLContext 621 | # in current versions of Python 622 | 623 | if context is None: 624 | if hasattr(ssl, 'create_default_context'): 625 | context = ssl.create_default_context() 626 | else: 627 | raise ValueError('SSL/TLS context must be specified') 628 | 629 | self._ssl = True 630 | self._ssl_context = context 631 | 632 | # Ensure _tls_insecure is consistent with check_hostname attribute 633 | if hasattr(context, 'check_hostname'): 634 | self._tls_insecure = not context.check_hostname 635 | 636 | def tls_set(self, ca_certs=None, certfile=None, keyfile=None, cert_reqs=None, tls_version=None, ciphers=None): 637 | """Configure network encryption and authentication options. Enables SSL/TLS support. 638 | 639 | ca_certs : a string path to the Certificate Authority certificate files 640 | that are to be treated as trusted by this client. If this is the only 641 | option given then the client will operate in a similar manner to a web 642 | browser. That is to say it will require the broker to have a 643 | certificate signed by the Certificate Authorities in ca_certs and will 644 | communicate using TLS v1, but will not attempt any form of 645 | authentication. This provides basic network encryption but may not be 646 | sufficient depending on how the broker is configured. 647 | By default, on Python 2.7.9+ or 3.4+, the default certification 648 | authority of the system is used. On older Python version this parameter 649 | is mandatory. 650 | 651 | certfile and keyfile are strings pointing to the PEM encoded client 652 | certificate and private keys respectively. If these arguments are not 653 | None then they will be used as client information for TLS based 654 | authentication. Support for this feature is broker dependent. Note 655 | that if either of these files in encrypted and needs a password to 656 | decrypt it, Python will ask for the password at the command line. It is 657 | not currently possible to define a callback to provide the password. 658 | 659 | cert_reqs allows the certificate requirements that the client imposes 660 | on the broker to be changed. By default this is ssl.CERT_REQUIRED, 661 | which means that the broker must provide a certificate. See the ssl 662 | pydoc for more information on this parameter. 663 | 664 | tls_version allows the version of the SSL/TLS protocol used to be 665 | specified. By default TLS v1 is used. Previous versions (all versions 666 | beginning with SSL) are possible but not recommended due to possible 667 | security problems. 668 | 669 | ciphers is a string specifying which encryption ciphers are allowable 670 | for this connection, or None to use the defaults. See the ssl pydoc for 671 | more information. 672 | 673 | Must be called before connect() or connect_async().""" 674 | if ssl is None: 675 | raise ValueError('This platform has no SSL/TLS.') 676 | 677 | if not hasattr(ssl, 'SSLContext'): 678 | # Require Python version that has SSL context support in standard library 679 | raise ValueError('Python 2.7.9 and 3.2 are the minimum supported versions for TLS.') 680 | 681 | if ca_certs is None and not hasattr(ssl.SSLContext, 'load_default_certs'): 682 | raise ValueError('ca_certs must not be None.') 683 | 684 | # Create SSLContext object 685 | if tls_version is None: 686 | tls_version = ssl.PROTOCOL_TLSv1 687 | # If the python version supports it, use highest TLS version automatically 688 | if hasattr(ssl, "PROTOCOL_TLS"): 689 | tls_version = ssl.PROTOCOL_TLS 690 | context = ssl.SSLContext(tls_version) 691 | 692 | # Configure context 693 | if certfile is not None: 694 | context.load_cert_chain(certfile, keyfile) 695 | 696 | if cert_reqs == ssl.CERT_NONE and hasattr(context, 'check_hostname'): 697 | context.check_hostname = False 698 | 699 | context.verify_mode = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs 700 | 701 | if ca_certs is not None: 702 | context.load_verify_locations(ca_certs) 703 | else: 704 | context.load_default_certs() 705 | 706 | if ciphers is not None: 707 | context.set_ciphers(ciphers) 708 | 709 | self.tls_set_context(context) 710 | 711 | if cert_reqs != ssl.CERT_NONE: 712 | # Default to secure, sets context.check_hostname attribute 713 | # if available 714 | self.tls_insecure_set(False) 715 | else: 716 | # But with ssl.CERT_NONE, we can not check_hostname 717 | self.tls_insecure_set(True) 718 | 719 | def tls_insecure_set(self, value): 720 | """Configure verification of the server hostname in the server certificate. 721 | 722 | If value is set to true, it is impossible to guarantee that the host 723 | you are connecting to is not impersonating your server. This can be 724 | useful in initial server testing, but makes it possible for a malicious 725 | third party to impersonate your server through DNS spoofing, for 726 | example. 727 | 728 | Do not use this function in a real system. Setting value to true means 729 | there is no point using encryption. 730 | 731 | Must be called before connect() and after either tls_set() or 732 | tls_set_context().""" 733 | 734 | if self._ssl_context is None: 735 | raise ValueError('Must configure SSL context before using tls_insecure_set.') 736 | 737 | self._tls_insecure = value 738 | 739 | # Ensure check_hostname is consistent with _tls_insecure attribute 740 | if hasattr(self._ssl_context, 'check_hostname'): 741 | # Rely on SSLContext to check host name 742 | # If verify_mode is CERT_NONE then the host name will never be checked 743 | self._ssl_context.check_hostname = not value 744 | 745 | def enable_logger(self, logger=None): 746 | if not logger: 747 | if self._logger: 748 | # Do not replace existing logger 749 | return 750 | logger = logging.getLogger(__name__) 751 | self._logger = logger 752 | 753 | def disable_logger(self): 754 | self._logger = None 755 | 756 | def connect(self, host, port=1883, keepalive=60, bind_address=""): 757 | """Connect to a remote broker. 758 | 759 | host is the hostname or IP address of the remote broker. 760 | port is the network port of the server host to connect to. Defaults to 761 | 1883. Note that the default port for MQTT over SSL/TLS is 8883 so if you 762 | are using tls_set() the port may need providing. 763 | keepalive: Maximum period in seconds between communications with the 764 | broker. If no other messages are being exchanged, this controls the 765 | rate at which the client will send ping messages to the broker. 766 | """ 767 | self.connect_async(host, port, keepalive, bind_address) 768 | return self.reconnect() 769 | 770 | def connect_srv(self, domain=None, keepalive=60, bind_address=""): 771 | """Connect to a remote broker. 772 | 773 | domain is the DNS domain to search for SRV records; if None, 774 | try to determine local domain name. 775 | keepalive and bind_address are as for connect() 776 | """ 777 | 778 | if HAVE_DNS is False: 779 | raise ValueError('No DNS resolver library found, try "pip install dnspython" or "pip3 install dnspython3".') 780 | 781 | if domain is None: 782 | domain = socket.getfqdn() 783 | domain = domain[domain.find('.') + 1:] 784 | 785 | try: 786 | rr = '_mqtt._tcp.%s' % domain 787 | if self._ssl: 788 | # IANA specifies secure-mqtt (not mqtts) for port 8883 789 | rr = '_secure-mqtt._tcp.%s' % domain 790 | answers = [] 791 | for answer in dns.resolver.query(rr, dns.rdatatype.SRV): 792 | addr = answer.target.to_text()[:-1] 793 | answers.append((addr, answer.port, answer.priority, answer.weight)) 794 | except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers): 795 | raise ValueError("No answer/NXDOMAIN for SRV in %s" % (domain)) 796 | 797 | # FIXME: doesn't account for weight 798 | for answer in answers: 799 | host, port, prio, weight = answer 800 | 801 | try: 802 | return self.connect(host, port, keepalive, bind_address) 803 | except: 804 | pass 805 | 806 | raise ValueError("No SRV hosts responded") 807 | 808 | def connect_async(self, host, port=1883, keepalive=60, bind_address=""): 809 | """Connect to a remote broker asynchronously. This is a non-blocking 810 | connect call that can be used with loop_start() to provide very quick 811 | start. 812 | 813 | host is the hostname or IP address of the remote broker. 814 | port is the network port of the server host to connect to. Defaults to 815 | 1883. Note that the default port for MQTT over SSL/TLS is 8883 so if you 816 | are using tls_set() the port may need providing. 817 | keepalive: Maximum period in seconds between communications with the 818 | broker. If no other messages are being exchanged, this controls the 819 | rate at which the client will send ping messages to the broker. 820 | """ 821 | if host is None or len(host) == 0: 822 | raise ValueError('Invalid host.') 823 | if port <= 0: 824 | raise ValueError('Invalid port number.') 825 | if keepalive < 0: 826 | raise ValueError('Keepalive must be >=0.') 827 | if bind_address != "" and bind_address is not None: 828 | if (sys.version_info[0] == 2 and sys.version_info[1] < 7) or ( 829 | sys.version_info[0] == 3 and sys.version_info[1] < 2): 830 | raise ValueError('bind_address requires Python 2.7 or 3.2.') 831 | 832 | self._host = host 833 | self._port = port 834 | self._keepalive = keepalive 835 | self._bind_address = bind_address 836 | 837 | self._state = mqtt_cs_connect_async 838 | 839 | def reconnect_delay_set(self, min_delay=1, max_delay=120): 840 | """ Configure the exponential reconnect delay 841 | 842 | When connection is lost, wait initially min_delay seconds and 843 | double this time every attempt. The wait is capped at max_delay. 844 | Once the client is fully connected (e.g. not only TCP socket, but 845 | received a success CONNACK), the wait timer is reset to min_delay. 846 | """ 847 | with self._reconnect_delay_mutex: 848 | self._reconnect_min_delay = min_delay 849 | self._reconnect_max_delay = max_delay 850 | self._reconnect_delay = None 851 | 852 | def reconnect(self): 853 | """Reconnect the client after a disconnect. Can only be called after 854 | connect()/connect_async().""" 855 | if len(self._host) == 0: 856 | raise ValueError('Invalid host.') 857 | if self._port <= 0: 858 | raise ValueError('Invalid port number.') 859 | 860 | self._in_packet = { 861 | "command": 0, 862 | "have_remaining": 0, 863 | "remaining_count": [], 864 | "remaining_mult": 1, 865 | "remaining_length": 0, 866 | "packet": b"", 867 | "to_process": 0, 868 | "pos": 0} 869 | 870 | with self._out_packet_mutex: 871 | self._out_packet = collections.deque() 872 | 873 | with self._current_out_packet_mutex: 874 | self._current_out_packet = None 875 | 876 | with self._msgtime_mutex: 877 | self._last_msg_in = time_func() 878 | self._last_msg_out = time_func() 879 | 880 | self._ping_t = 0 881 | self._state = mqtt_cs_new 882 | 883 | if self._sock: 884 | self._sock.close() 885 | self._sock = None 886 | 887 | # Put messages in progress in a valid state. 888 | self._messages_reconnect_reset() 889 | 890 | try: 891 | if (sys.version_info[0] == 2 and sys.version_info[1] < 7) or ( 892 | sys.version_info[0] == 3 and sys.version_info[1] < 2): 893 | sock = socket.create_connection((self._host, self._port)) 894 | else: 895 | sock = socket.create_connection((self._host, self._port), source_address=(self._bind_address, 0)) 896 | except socket.error as err: 897 | if err.errno != errno.EINPROGRESS and err.errno != errno.EWOULDBLOCK and err.errno != EAGAIN: 898 | raise 899 | 900 | if self._ssl: 901 | # SSL is only supported when SSLContext is available (implies Python >= 2.7.9 or >= 3.2) 902 | 903 | verify_host = not self._tls_insecure 904 | try: 905 | # Try with server_hostname, even it's not supported in certain scenarios 906 | sock = self._ssl_context.wrap_socket( 907 | sock, 908 | server_hostname=self._host, 909 | do_handshake_on_connect=False, 910 | ) 911 | except ssl.CertificateError: 912 | # CertificateError is derived from ValueError 913 | raise 914 | except ValueError: 915 | # Python version requires SNI in order to handle server_hostname, but SNI is not available 916 | sock = self._ssl_context.wrap_socket( 917 | sock, 918 | do_handshake_on_connect=False, 919 | ) 920 | else: 921 | # If SSL context has already checked hostname, then don't need to do it again 922 | if (hasattr(self._ssl_context, 'check_hostname') and 923 | self._ssl_context.check_hostname): 924 | verify_host = False 925 | 926 | sock.settimeout(self._keepalive) 927 | sock.do_handshake() 928 | 929 | if verify_host: 930 | ssl.match_hostname(sock.getpeercert(), self._host) 931 | 932 | if self._transport == "websockets": 933 | sock.settimeout(self._keepalive) 934 | sock = WebsocketWrapper(sock, self._host, self._port, self._ssl, 935 | self._websocket_path, self._websocket_extra_headers) 936 | 937 | self._sock = sock 938 | self._sock.setblocking(0) 939 | 940 | return self._send_connect(self._keepalive, self._clean_session) 941 | 942 | def loop(self, timeout=1.0, max_packets=1): 943 | """Process network events. 944 | 945 | This function must be called regularly to ensure communication with the 946 | broker is carried out. It calls select() on the network socket to wait 947 | for network events. If incoming data is present it will then be 948 | processed. Outgoing commands, from e.g. publish(), are normally sent 949 | immediately that their function is called, but this is not always 950 | possible. loop() will also attempt to send any remaining outgoing 951 | messages, which also includes commands that are part of the flow for 952 | messages with QoS>0. 953 | 954 | timeout: The time in seconds to wait for incoming/outgoing network 955 | traffic before timing out and returning. 956 | max_packets: Not currently used. 957 | 958 | Returns MQTT_ERR_SUCCESS on success. 959 | Returns >0 on error. 960 | 961 | A ValueError will be raised if timeout < 0""" 962 | if timeout < 0.0: 963 | raise ValueError('Invalid timeout.') 964 | 965 | with self._current_out_packet_mutex: 966 | with self._out_packet_mutex: 967 | if self._current_out_packet is None and len(self._out_packet) > 0: 968 | self._current_out_packet = self._out_packet.popleft() 969 | 970 | if self._current_out_packet: 971 | wlist = [self._sock] 972 | else: 973 | wlist = [] 974 | 975 | # used to check if there are any bytes left in the (SSL) socket 976 | pending_bytes = 0 977 | if hasattr(self._sock, 'pending'): 978 | pending_bytes = self._sock.pending() 979 | 980 | # if bytes are pending do not wait in select 981 | if pending_bytes > 0: 982 | timeout = 0.0 983 | 984 | # sockpairR is used to break out of select() before the timeout, on a 985 | # call to publish() etc. 986 | rlist = [self._sock, self._sockpairR] 987 | try: 988 | socklist = select.select(rlist, wlist, [], timeout) 989 | except TypeError: 990 | # Socket isn't correct type, in likelihood connection is lost 991 | return MQTT_ERR_CONN_LOST 992 | except ValueError: 993 | # Can occur if we just reconnected but rlist/wlist contain a -1 for 994 | # some reason. 995 | return MQTT_ERR_CONN_LOST 996 | except KeyboardInterrupt: 997 | # Allow ^C to interrupt 998 | raise 999 | except: 1000 | return MQTT_ERR_UNKNOWN 1001 | 1002 | if self._sock in socklist[0] or pending_bytes > 0: 1003 | rc = self.loop_read(max_packets) 1004 | if rc or self._sock is None: 1005 | return rc 1006 | 1007 | if self._sockpairR in socklist[0]: 1008 | # Stimulate output write even though we didn't ask for it, because 1009 | # at that point the publish or other command wasn't present. 1010 | socklist[1].insert(0, self._sock) 1011 | # Clear sockpairR - only ever a single byte written. 1012 | try: 1013 | self._sockpairR.recv(1) 1014 | except socket.error as err: 1015 | if err.errno != EAGAIN: 1016 | raise 1017 | 1018 | if self._sock in socklist[1]: 1019 | rc = self.loop_write(max_packets) 1020 | if rc or self._sock is None: 1021 | return rc 1022 | 1023 | return self.loop_misc() 1024 | 1025 | def publish(self, topic, payload=None, qos=0, retain=False): 1026 | """Publish a message on a topic. 1027 | 1028 | This causes a message to be sent to the broker and subsequently from 1029 | the broker to any clients subscribing to matching topics. 1030 | 1031 | topic: The topic that the message should be published on. 1032 | payload: The actual message to send. If not given, or set to None a 1033 | zero length message will be used. Passing an int or float will result 1034 | in the payload being converted to a string representing that number. If 1035 | you wish to send a true int/float, use struct.pack() to create the 1036 | payload you require. 1037 | qos: The quality of service level to use. 1038 | retain: If set to true, the message will be set as the "last known 1039 | good"/retained message for the topic. 1040 | 1041 | Returns a MQTTMessageInfo class, which can be used to determine whether 1042 | the message has been delivered (using info.is_published()) or to block 1043 | waiting for the message to be delivered (info.wait_for_publish()). The 1044 | message ID and return code of the publish() call can be found at 1045 | info.mid and info.rc. 1046 | 1047 | For backwards compatibility, the MQTTMessageInfo class is iterable so 1048 | the old construct of (rc, mid) = client.publish(...) is still valid. 1049 | 1050 | rc is MQTT_ERR_SUCCESS to indicate success or MQTT_ERR_NO_CONN if the 1051 | client is not currently connected. mid is the message ID for the 1052 | publish request. The mid value can be used to track the publish request 1053 | by checking against the mid argument in the on_publish() callback if it 1054 | is defined. 1055 | 1056 | A ValueError will be raised if topic is None, has zero length or is 1057 | invalid (contains a wildcard), if qos is not one of 0, 1 or 2, or if 1058 | the length of the payload is greater than 268435455 bytes.""" 1059 | if topic is None or len(topic) == 0: 1060 | raise ValueError('Invalid topic.') 1061 | 1062 | topic = topic.encode('utf-8') 1063 | 1064 | if self._topic_wildcard_len_check(topic) != MQTT_ERR_SUCCESS: 1065 | raise ValueError('Publish topic cannot contain wildcards.') 1066 | 1067 | if qos < 0 or qos > 2: 1068 | raise ValueError('Invalid QoS level.') 1069 | 1070 | if isinstance(payload, unicode): 1071 | local_payload = payload.encode('utf-8') 1072 | elif isinstance(payload, (bytes, bytearray)): 1073 | local_payload = payload 1074 | elif isinstance(payload, (int, float)): 1075 | local_payload = str(payload).encode('ascii') 1076 | elif payload is None: 1077 | local_payload = b'' 1078 | else: 1079 | raise TypeError('payload must be a string, bytearray, int, float or None.') 1080 | 1081 | if len(local_payload) > 268435455: 1082 | raise ValueError('Payload too large.') 1083 | 1084 | local_mid = self._mid_generate() 1085 | 1086 | if qos == 0: 1087 | info = MQTTMessageInfo(local_mid) 1088 | rc = self._send_publish(local_mid, topic, local_payload, qos, retain, False, info) 1089 | info.rc = rc 1090 | return info 1091 | else: 1092 | message = MQTTMessage(local_mid, topic) 1093 | message.timestamp = time_func() 1094 | message.payload = local_payload 1095 | message.qos = qos 1096 | message.retain = retain 1097 | message.dup = False 1098 | 1099 | with self._out_message_mutex: 1100 | if self._max_queued_messages > 0 and len(self._out_messages) >= self._max_queued_messages: 1101 | message.info.rc = MQTT_ERR_QUEUE_SIZE 1102 | return message.info 1103 | 1104 | self._out_messages.append(message) 1105 | if self._max_inflight_messages == 0 or self._inflight_messages < self._max_inflight_messages: 1106 | self._inflight_messages += 1 1107 | if qos == 1: 1108 | message.state = mqtt_ms_wait_for_puback 1109 | elif qos == 2: 1110 | message.state = mqtt_ms_wait_for_pubrec 1111 | 1112 | rc = self._send_publish(message.mid, topic, message.payload, message.qos, message.retain, 1113 | message.dup) 1114 | 1115 | # remove from inflight messages so it will be send after a connection is made 1116 | if rc is MQTT_ERR_NO_CONN: 1117 | self._inflight_messages -= 1 1118 | message.state = mqtt_ms_publish 1119 | 1120 | message.info.rc = rc 1121 | return message.info 1122 | else: 1123 | message.state = mqtt_ms_queued 1124 | message.info.rc = MQTT_ERR_SUCCESS 1125 | return message.info 1126 | 1127 | def username_pw_set(self, username, password=None): 1128 | """Set a username and optionally a password for broker authentication. 1129 | 1130 | Must be called before connect() to have any effect. 1131 | Requires a broker that supports MQTT v3.1. 1132 | 1133 | username: The username to authenticate with. Need have no relationship to the client id. Must be unicode 1134 | [MQTT-3.1.3-11]. 1135 | password: The password to authenticate with. Optional, set to None if not required. If it is unicode, then it 1136 | will be encoded as UTF-8. 1137 | """ 1138 | 1139 | # [MQTT-3.1.3-11] User name must be UTF-8 encoded string 1140 | self._username = username.encode('utf-8') 1141 | self._password = password 1142 | if isinstance(self._password, unicode): 1143 | self._password = self._password.encode('utf-8') 1144 | 1145 | def disconnect(self): 1146 | """Disconnect a connected client from the broker.""" 1147 | self._state = mqtt_cs_disconnecting 1148 | 1149 | if self._sock is None: 1150 | return MQTT_ERR_NO_CONN 1151 | 1152 | return self._send_disconnect() 1153 | 1154 | def subscribe(self, topic, qos=0): 1155 | """Subscribe the client to one or more topics. 1156 | 1157 | This function may be called in three different ways: 1158 | 1159 | Simple string and integer 1160 | ------------------------- 1161 | e.g. subscribe("my/topic", 2) 1162 | 1163 | topic: A string specifying the subscription topic to subscribe to. 1164 | qos: The desired quality of service level for the subscription. 1165 | Defaults to 0. 1166 | 1167 | String and integer tuple 1168 | ------------------------ 1169 | e.g. subscribe(("my/topic", 1)) 1170 | 1171 | topic: A tuple of (topic, qos). Both topic and qos must be present in 1172 | the tuple. 1173 | qos: Not used. 1174 | 1175 | List of string and integer tuples 1176 | ------------------------ 1177 | e.g. subscribe([("my/topic", 0), ("another/topic", 2)]) 1178 | 1179 | This allows multiple topic subscriptions in a single SUBSCRIPTION 1180 | command, which is more efficient than using multiple calls to 1181 | subscribe(). 1182 | 1183 | topic: A list of tuple of format (topic, qos). Both topic and qos must 1184 | be present in all of the tuples. 1185 | qos: Not used. 1186 | 1187 | The function returns a tuple (result, mid), where result is 1188 | MQTT_ERR_SUCCESS to indicate success or (MQTT_ERR_NO_CONN, None) if the 1189 | client is not currently connected. mid is the message ID for the 1190 | subscribe request. The mid value can be used to track the subscribe 1191 | request by checking against the mid argument in the on_subscribe() 1192 | callback if it is defined. 1193 | 1194 | Raises a ValueError if qos is not 0, 1 or 2, or if topic is None or has 1195 | zero string length, or if topic is not a string, tuple or list. 1196 | """ 1197 | topic_qos_list = None 1198 | 1199 | if isinstance(topic, tuple): 1200 | topic, qos = topic 1201 | 1202 | if isinstance(topic, basestring): 1203 | if qos < 0 or qos > 2: 1204 | raise ValueError('Invalid QoS level.') 1205 | if topic is None or len(topic) == 0: 1206 | raise ValueError('Invalid topic.') 1207 | topic_qos_list = [(topic.encode('utf-8'), qos)] 1208 | elif isinstance(topic, list): 1209 | topic_qos_list = [] 1210 | for t, q in topic: 1211 | if q < 0 or q > 2: 1212 | raise ValueError('Invalid QoS level.') 1213 | if t is None or len(t) == 0 or not isinstance(t, basestring): 1214 | raise ValueError('Invalid topic.') 1215 | topic_qos_list.append((t.encode('utf-8'), q)) 1216 | 1217 | if topic_qos_list is None: 1218 | raise ValueError("No topic specified, or incorrect topic type.") 1219 | 1220 | if any(self._filter_wildcard_len_check(topic) != MQTT_ERR_SUCCESS for topic, _ in topic_qos_list): 1221 | raise ValueError('Invalid subscription filter.') 1222 | 1223 | if self._sock is None: 1224 | return (MQTT_ERR_NO_CONN, None) 1225 | 1226 | return self._send_subscribe(False, topic_qos_list) 1227 | 1228 | def unsubscribe(self, topic): 1229 | """Unsubscribe the client from one or more topics. 1230 | 1231 | topic: A single string, or list of strings that are the subscription 1232 | topics to unsubscribe from. 1233 | 1234 | Returns a tuple (result, mid), where result is MQTT_ERR_SUCCESS 1235 | to indicate success or (MQTT_ERR_NO_CONN, None) if the client is not 1236 | currently connected. 1237 | mid is the message ID for the unsubscribe request. The mid value can be 1238 | used to track the unsubscribe request by checking against the mid 1239 | argument in the on_unsubscribe() callback if it is defined. 1240 | 1241 | Raises a ValueError if topic is None or has zero string length, or is 1242 | not a string or list. 1243 | """ 1244 | topic_list = None 1245 | if topic is None: 1246 | raise ValueError('Invalid topic.') 1247 | if isinstance(topic, basestring): 1248 | if len(topic) == 0: 1249 | raise ValueError('Invalid topic.') 1250 | topic_list = [topic.encode('utf-8')] 1251 | elif isinstance(topic, list): 1252 | topic_list = [] 1253 | for t in topic: 1254 | if len(t) == 0 or not isinstance(t, basestring): 1255 | raise ValueError('Invalid topic.') 1256 | topic_list.append(t.encode('utf-8')) 1257 | 1258 | if topic_list is None: 1259 | raise ValueError("No topic specified, or incorrect topic type.") 1260 | 1261 | if self._sock is None: 1262 | return (MQTT_ERR_NO_CONN, None) 1263 | 1264 | return self._send_unsubscribe(False, topic_list) 1265 | 1266 | def loop_read(self, max_packets=1): 1267 | """Process read network events. Use in place of calling loop() if you 1268 | wish to handle your client reads as part of your own application. 1269 | 1270 | Use socket() to obtain the client socket to call select() or equivalent 1271 | on. 1272 | 1273 | Do not use if you are using the threaded interface loop_start().""" 1274 | if self._sock is None: 1275 | return MQTT_ERR_NO_CONN 1276 | 1277 | max_packets = len(self._out_messages) + len(self._in_messages) 1278 | if max_packets < 1: 1279 | max_packets = 1 1280 | 1281 | for _ in range(0, max_packets): 1282 | if self._sock is None: 1283 | return MQTT_ERR_NO_CONN 1284 | rc = self._packet_read() 1285 | if rc > 0: 1286 | return self._loop_rc_handle(rc) 1287 | elif rc == MQTT_ERR_AGAIN: 1288 | return MQTT_ERR_SUCCESS 1289 | return MQTT_ERR_SUCCESS 1290 | 1291 | def loop_write(self, max_packets=1): 1292 | """Process write network events. Use in place of calling loop() if you 1293 | wish to handle your client writes as part of your own application. 1294 | 1295 | Use socket() to obtain the client socket to call select() or equivalent 1296 | on. 1297 | 1298 | Use want_write() to determine if there is data waiting to be written. 1299 | 1300 | Do not use if you are using the threaded interface loop_start().""" 1301 | if self._sock is None: 1302 | return MQTT_ERR_NO_CONN 1303 | 1304 | max_packets = len(self._out_packet) + 1 1305 | if max_packets < 1: 1306 | max_packets = 1 1307 | 1308 | for _ in range(0, max_packets): 1309 | rc = self._packet_write() 1310 | if rc > 0: 1311 | return self._loop_rc_handle(rc) 1312 | elif rc == MQTT_ERR_AGAIN: 1313 | return MQTT_ERR_SUCCESS 1314 | return MQTT_ERR_SUCCESS 1315 | 1316 | def want_write(self): 1317 | """Call to determine if there is network data waiting to be written. 1318 | Useful if you are calling select() yourself rather than using loop(). 1319 | """ 1320 | if self._current_out_packet or len(self._out_packet) > 0: 1321 | return True 1322 | else: 1323 | return False 1324 | 1325 | def loop_misc(self): 1326 | """Process miscellaneous network events. Use in place of calling loop() if you 1327 | wish to call select() or equivalent on. 1328 | 1329 | Do not use if you are using the threaded interface loop_start().""" 1330 | if self._sock is None: 1331 | return MQTT_ERR_NO_CONN 1332 | 1333 | now = time_func() 1334 | self._check_keepalive() 1335 | if self._last_retry_check + 1 < now: 1336 | # Only check once a second at most 1337 | self._message_retry_check() 1338 | self._last_retry_check = now 1339 | 1340 | if self._ping_t > 0 and now - self._ping_t >= self._keepalive: 1341 | # client->ping_t != 0 means we are waiting for a pingresp. 1342 | # This hasn't happened in the keepalive time so we should disconnect. 1343 | if self._sock: 1344 | self._sock.close() 1345 | self._sock = None 1346 | 1347 | if self._state == mqtt_cs_disconnecting: 1348 | rc = MQTT_ERR_SUCCESS 1349 | else: 1350 | rc = 1 1351 | 1352 | with self._callback_mutex: 1353 | if self.on_disconnect: 1354 | with self._in_callback: 1355 | self.on_disconnect(self, self._userdata, rc) 1356 | 1357 | return MQTT_ERR_CONN_LOST 1358 | 1359 | return MQTT_ERR_SUCCESS 1360 | 1361 | def max_inflight_messages_set(self, inflight): 1362 | """Set the maximum number of messages with QoS>0 that can be part way 1363 | through their network flow at once. Defaults to 20.""" 1364 | if inflight < 0: 1365 | raise ValueError('Invalid inflight.') 1366 | self._max_inflight_messages = inflight 1367 | 1368 | def max_queued_messages_set(self, queue_size): 1369 | """Set the maximum number of messages in the outgoing message queue. 1370 | 0 means unlimited.""" 1371 | if queue_size < 0: 1372 | raise ValueError('Invalid queue size.') 1373 | if not isinstance(queue_size, int): 1374 | raise ValueError('Invalid type of queue size.') 1375 | self._max_queued_messages = queue_size 1376 | return self 1377 | 1378 | def message_retry_set(self, retry): 1379 | """Set the timeout in seconds before a message with QoS>0 is retried. 1380 | 20 seconds by default.""" 1381 | if retry < 0: 1382 | raise ValueError('Invalid retry.') 1383 | 1384 | self._message_retry = retry 1385 | 1386 | def user_data_set(self, userdata): 1387 | """Set the user data variable passed to callbacks. May be any data type.""" 1388 | self._userdata = userdata 1389 | 1390 | def will_set(self, topic, payload=None, qos=0, retain=False): 1391 | """Set a Will to be sent by the broker in case the client disconnects unexpectedly. 1392 | 1393 | This must be called before connect() to have any effect. 1394 | 1395 | topic: The topic that the will message should be published on. 1396 | payload: The message to send as a will. If not given, or set to None a 1397 | zero length message will be used as the will. Passing an int or float 1398 | will result in the payload being converted to a string representing 1399 | that number. If you wish to send a true int/float, use struct.pack() to 1400 | create the payload you require. 1401 | qos: The quality of service level to use for the will. 1402 | retain: If set to true, the will message will be set as the "last known 1403 | good"/retained message for the topic. 1404 | 1405 | Raises a ValueError if qos is not 0, 1 or 2, or if topic is None or has 1406 | zero string length. 1407 | """ 1408 | if topic is None or len(topic) == 0: 1409 | raise ValueError('Invalid topic.') 1410 | 1411 | if qos < 0 or qos > 2: 1412 | raise ValueError('Invalid QoS level.') 1413 | 1414 | if isinstance(payload, unicode): 1415 | self._will_payload = payload.encode('utf-8') 1416 | elif isinstance(payload, (bytes, bytearray)): 1417 | self._will_payload = payload 1418 | elif isinstance(payload, (int, float)): 1419 | self._will_payload = str(payload).encode('ascii') 1420 | elif payload is None: 1421 | self._will_payload = b"" 1422 | else: 1423 | raise TypeError('payload must be a string, bytearray, int, float or None.') 1424 | 1425 | self._will = True 1426 | self._will_topic = topic.encode('utf-8') 1427 | self._will_qos = qos 1428 | self._will_retain = retain 1429 | 1430 | def will_clear(self): 1431 | """ Removes a will that was previously configured with will_set(). 1432 | 1433 | Must be called before connect() to have any effect.""" 1434 | self._will = False 1435 | self._will_topic = b"" 1436 | self._will_payload = b"" 1437 | self._will_qos = 0 1438 | self._will_retain = False 1439 | 1440 | def socket(self): 1441 | """Return the socket or ssl object for this client.""" 1442 | return self._sock 1443 | 1444 | def loop_forever(self, timeout=1.0, max_packets=1, retry_first_connection=False): 1445 | """This function call loop() for you in an infinite blocking loop. It 1446 | is useful for the case where you only want to run the MQTT client loop 1447 | in your program. 1448 | 1449 | loop_forever() will handle reconnecting for you. If you call 1450 | disconnect() in a callback it will return. 1451 | 1452 | 1453 | timeout: The time in seconds to wait for incoming/outgoing network 1454 | traffic before timing out and returning. 1455 | max_packets: Not currently used. 1456 | retry_first_connection: Should the first connection attempt be retried on failure. 1457 | 1458 | Raises socket.error on first connection failures unless retry_first_connection=True 1459 | """ 1460 | 1461 | run = True 1462 | 1463 | while run: 1464 | if self._thread_terminate is True: 1465 | break 1466 | 1467 | if self._state == mqtt_cs_connect_async: 1468 | try: 1469 | self.reconnect() 1470 | except (socket.error, WebsocketConnectionError): 1471 | if not retry_first_connection: 1472 | raise 1473 | self._easy_log(MQTT_LOG_DEBUG, "Connection failed, retrying") 1474 | self._reconnect_wait() 1475 | else: 1476 | break 1477 | 1478 | while run: 1479 | rc = MQTT_ERR_SUCCESS 1480 | while rc == MQTT_ERR_SUCCESS: 1481 | rc = self.loop(timeout, max_packets) 1482 | # We don't need to worry about locking here, because we've 1483 | # either called loop_forever() when in single threaded mode, or 1484 | # in multi threaded mode when loop_stop() has been called and 1485 | # so no other threads can access _current_out_packet, 1486 | # _out_packet or _messages. 1487 | if (self._thread_terminate is True 1488 | and self._current_out_packet is None 1489 | and len(self._out_packet) == 0 1490 | and len(self._out_messages) == 0): 1491 | rc = 1 1492 | run = False 1493 | 1494 | 1495 | def should_exit(): 1496 | return self._state == mqtt_cs_disconnecting or run is False or self._thread_terminate is True 1497 | 1498 | if should_exit(): 1499 | run = False 1500 | else: 1501 | self._reconnect_wait() 1502 | 1503 | if should_exit(): 1504 | run = False 1505 | else: 1506 | try: 1507 | self.reconnect() 1508 | except (socket.error, WebsocketConnectionError): 1509 | pass 1510 | 1511 | return rc 1512 | 1513 | def loop_start(self): 1514 | """This is part of the threaded client interface. Call this once to 1515 | start a new thread to process network traffic. This provides an 1516 | alternative to repeatedly calling loop() yourself. 1517 | """ 1518 | if self._thread is not None: 1519 | return MQTT_ERR_INVAL 1520 | 1521 | self._thread_terminate = False 1522 | self._thread = threading.Thread(target=self._thread_main) 1523 | self._thread.daemon = True 1524 | self._thread.start() 1525 | 1526 | def loop_stop(self, force=False): 1527 | """This is part of the threaded client interface. Call this once to 1528 | stop the network thread previously created with loop_start(). This call 1529 | will block until the network thread finishes. 1530 | 1531 | The force parameter is currently ignored. 1532 | """ 1533 | if self._thread is None: 1534 | return MQTT_ERR_INVAL 1535 | 1536 | self._thread_terminate = True 1537 | if threading.current_thread() != self._thread: 1538 | self._thread.join() 1539 | self._thread = None 1540 | 1541 | @property 1542 | def on_log(self): 1543 | """If implemented, called when the client has log information. 1544 | Defined to allow debugging.""" 1545 | return self._on_log 1546 | 1547 | @on_log.setter 1548 | def on_log(self, func): 1549 | """ Define the logging callback implementation. 1550 | 1551 | Expected signature is: 1552 | log_callback(client, userdata, level, buf) 1553 | 1554 | client: the client instance for this callback 1555 | userdata: the private user data as set in Client() or userdata_set() 1556 | level: gives the severity of the message and will be one of 1557 | MQTT_LOG_INFO, MQTT_LOG_NOTICE, MQTT_LOG_WARNING, 1558 | MQTT_LOG_ERR, and MQTT_LOG_DEBUG. 1559 | buf: the message itself 1560 | """ 1561 | self._on_log = func 1562 | 1563 | @property 1564 | def on_connect(self): 1565 | """If implemented, called when the broker responds to our connection 1566 | request.""" 1567 | return self._on_connect 1568 | 1569 | @on_connect.setter 1570 | def on_connect(self, func): 1571 | """ Define the connect callback implementation. 1572 | 1573 | Expected signature is: 1574 | connect_callback(client, userdata, flags, rc) 1575 | 1576 | client: the client instance for this callback 1577 | userdata: the private user data as set in Client() or userdata_set() 1578 | flags: response flags sent by the broker 1579 | rc: the connection result 1580 | 1581 | flags is a dict that contains response flags from the broker: 1582 | flags['session present'] - this flag is useful for clients that are 1583 | using clean session set to 0 only. If a client with clean 1584 | session=0, that reconnects to a broker that it has previously 1585 | connected to, this flag indicates whether the broker still has the 1586 | session information for the client. If 1, the session still exists. 1587 | 1588 | The value of rc indicates success or not: 1589 | 0: Connection successful 1590 | 1: Connection refused - incorrect protocol version 1591 | 2: Connection refused - invalid client identifier 1592 | 3: Connection refused - server unavailable 1593 | 4: Connection refused - bad username or password 1594 | 5: Connection refused - not authorised 1595 | 6-255: Currently unused. 1596 | """ 1597 | with self._callback_mutex: 1598 | self._on_connect = func 1599 | 1600 | @property 1601 | def on_subscribe(self): 1602 | """If implemented, called when the broker responds to a subscribe 1603 | request.""" 1604 | return self._on_subscribe 1605 | 1606 | @on_subscribe.setter 1607 | def on_subscribe(self, func): 1608 | """ Define the suscribe callback implementation. 1609 | 1610 | Expected signature is: 1611 | subscribe_callback(client, userdata, mid, granted_qos) 1612 | 1613 | client: the client instance for this callback 1614 | userdata: the private user data as set in Client() or userdata_set() 1615 | mid: matches the mid variable returned from the corresponding 1616 | subscribe() call. 1617 | granted_qos: list of integers that give the QoS level the broker has 1618 | granted for each of the different subscription requests. 1619 | """ 1620 | with self._callback_mutex: 1621 | self._on_subscribe = func 1622 | 1623 | @property 1624 | def on_message(self): 1625 | """If implemented, called when a message has been received on a topic 1626 | that the client subscribes to. 1627 | 1628 | This callback will be called for every message received. Use 1629 | message_callback_add() to define multiple callbacks that will be called 1630 | for specific topic filters.""" 1631 | return self._on_message 1632 | 1633 | @on_message.setter 1634 | def on_message(self, func): 1635 | """ Define the message received callback implementation. 1636 | 1637 | Expected signature is: 1638 | on_message_callback(client, userdata, message) 1639 | 1640 | client: the client instance for this callback 1641 | userdata: the private user data as set in Client() or userdata_set() 1642 | message: an instance of MQTTMessage. 1643 | This is a class with members topic, payload, qos, retain. 1644 | """ 1645 | with self._callback_mutex: 1646 | self._on_message = func 1647 | 1648 | @property 1649 | def on_publish(self): 1650 | """If implemented, called when a message that was to be sent using the 1651 | publish() call has completed transmission to the broker. 1652 | 1653 | For messages with QoS levels 1 and 2, this means that the appropriate 1654 | handshakes have completed. For QoS 0, this simply means that the message 1655 | has left the client. 1656 | This callback is important because even if the publish() call returns 1657 | success, it does not always mean that the message has been sent.""" 1658 | return self._on_publish 1659 | 1660 | @on_publish.setter 1661 | def on_publish(self, func): 1662 | """ Define the published message callback implementation. 1663 | 1664 | Expected signature is: 1665 | on_publish_callback(client, userdata, mid) 1666 | 1667 | client: the client instance for this callback 1668 | userdata: the private user data as set in Client() or userdata_set() 1669 | mid: matches the mid variable returned from the corresponding 1670 | publish() call, to allow outgoing messages to be tracked. 1671 | """ 1672 | with self._callback_mutex: 1673 | self._on_publish = func 1674 | 1675 | @property 1676 | def on_unsubscribe(self): 1677 | """If implemented, called when the broker responds to an unsubscribe 1678 | request.""" 1679 | return self._on_unsubscribe 1680 | 1681 | @on_unsubscribe.setter 1682 | def on_unsubscribe(self, func): 1683 | """ Define the unsubscribe callback implementation. 1684 | 1685 | Expected signature is: 1686 | unsubscribe_callback(client, userdata, mid) 1687 | 1688 | client: the client instance for this callback 1689 | userdata: the private user data as set in Client() or userdata_set() 1690 | mid: matches the mid variable returned from the corresponding 1691 | unsubscribe() call. 1692 | """ 1693 | with self._callback_mutex: 1694 | self._on_unsubscribe = func 1695 | 1696 | @property 1697 | def on_disconnect(self): 1698 | """If implemented, called when the client disconnects from the broker. 1699 | """ 1700 | return self._on_disconnect 1701 | 1702 | @on_disconnect.setter 1703 | def on_disconnect(self, func): 1704 | """ Define the disconnect callback implementation. 1705 | 1706 | Expected signature is: 1707 | disconnect_callback(client, userdata, self) 1708 | 1709 | client: the client instance for this callback 1710 | userdata: the private user data as set in Client() or userdata_set() 1711 | rc: the disconnection result 1712 | The rc parameter indicates the disconnection state. If 1713 | MQTT_ERR_SUCCESS (0), the callback was called in response to 1714 | a disconnect() call. If any other value the disconnection 1715 | was unexpected, such as might be caused by a network error. 1716 | """ 1717 | with self._callback_mutex: 1718 | self._on_disconnect = func 1719 | 1720 | def message_callback_add(self, sub, callback): 1721 | """Register a message callback for a specific topic. 1722 | Messages that match 'sub' will be passed to 'callback'. Any 1723 | non-matching messages will be passed to the default on_message 1724 | callback. 1725 | 1726 | Call multiple times with different 'sub' to define multiple topic 1727 | specific callbacks. 1728 | 1729 | Topic specific callbacks may be removed with 1730 | message_callback_remove().""" 1731 | if callback is None or sub is None: 1732 | raise ValueError("sub and callback must both be defined.") 1733 | 1734 | with self._callback_mutex: 1735 | self._on_message_filtered[sub] = callback 1736 | 1737 | def message_callback_remove(self, sub): 1738 | """Remove a message callback previously registered with 1739 | message_callback_add().""" 1740 | if sub is None: 1741 | raise ValueError("sub must defined.") 1742 | 1743 | with self._callback_mutex: 1744 | try: 1745 | del self._on_message_filtered[sub] 1746 | except KeyError: # no such subscription 1747 | pass 1748 | 1749 | # ============================================================ 1750 | # Private functions 1751 | # ============================================================ 1752 | 1753 | def _loop_rc_handle(self, rc): 1754 | if rc: 1755 | if self._sock: 1756 | self._sock.close() 1757 | self._sock = None 1758 | 1759 | if self._state == mqtt_cs_disconnecting: 1760 | rc = MQTT_ERR_SUCCESS 1761 | 1762 | with self._callback_mutex: 1763 | if self.on_disconnect: 1764 | with self._in_callback: 1765 | self.on_disconnect(self, self._userdata, rc) 1766 | return rc 1767 | 1768 | def _packet_read(self): 1769 | # This gets called if pselect() indicates that there is network data 1770 | # available - ie. at least one byte. What we do depends on what data we 1771 | # already have. 1772 | # If we've not got a command, attempt to read one and save it. This should 1773 | # always work because it's only a single byte. 1774 | # Then try to read the remaining length. This may fail because it is may 1775 | # be more than one byte - will need to save data pending next read if it 1776 | # does fail. 1777 | # Then try to read the remaining payload, where 'payload' here means the 1778 | # combined variable header and actual payload. This is the most likely to 1779 | # fail due to longer length, so save current data and current position. 1780 | # After all data is read, send to _mqtt_handle_packet() to deal with. 1781 | # Finally, free the memory and reset everything to starting conditions. 1782 | if self._in_packet['command'] == 0: 1783 | try: 1784 | command = self._sock.recv(1) 1785 | except socket.error as err: 1786 | if self._ssl and (err.errno == ssl.SSL_ERROR_WANT_READ or err.errno == ssl.SSL_ERROR_WANT_WRITE): 1787 | return MQTT_ERR_AGAIN 1788 | if err.errno == EAGAIN: 1789 | return MQTT_ERR_AGAIN 1790 | print(err) 1791 | return 1 1792 | else: 1793 | if len(command) == 0: 1794 | return 1 1795 | command, = struct.unpack("!B", command) 1796 | self._in_packet['command'] = command 1797 | 1798 | if self._in_packet['have_remaining'] == 0: 1799 | # Read remaining 1800 | # Algorithm for decoding taken from pseudo code at 1801 | # http://publib.boulder.ibm.com/infocenter/wmbhelp/v6r0m0/topic/com.ibm.etools.mft.doc/ac10870_.htm 1802 | while True: 1803 | try: 1804 | byte = self._sock.recv(1) 1805 | except socket.error as err: 1806 | if self._ssl and (err.errno == ssl.SSL_ERROR_WANT_READ or err.errno == ssl.SSL_ERROR_WANT_WRITE): 1807 | return MQTT_ERR_AGAIN 1808 | if err.errno == EAGAIN: 1809 | return MQTT_ERR_AGAIN 1810 | print(err) 1811 | return 1 1812 | else: 1813 | if len(byte) == 0: 1814 | return 1 1815 | byte, = struct.unpack("!B", byte) 1816 | self._in_packet['remaining_count'].append(byte) 1817 | # Max 4 bytes length for remaining length as defined by protocol. 1818 | # Anything more likely means a broken/malicious client. 1819 | if len(self._in_packet['remaining_count']) > 4: 1820 | return MQTT_ERR_PROTOCOL 1821 | 1822 | self._in_packet['remaining_length'] += (byte & 127) * self._in_packet['remaining_mult'] 1823 | self._in_packet['remaining_mult'] = self._in_packet['remaining_mult'] * 128 1824 | 1825 | if (byte & 128) == 0: 1826 | break 1827 | 1828 | self._in_packet['have_remaining'] = 1 1829 | self._in_packet['to_process'] = self._in_packet['remaining_length'] 1830 | 1831 | while self._in_packet['to_process'] > 0: 1832 | try: 1833 | data = self._sock.recv(self._in_packet['to_process']) 1834 | except socket.error as err: 1835 | if self._ssl and (err.errno == ssl.SSL_ERROR_WANT_READ or err.errno == ssl.SSL_ERROR_WANT_WRITE): 1836 | return MQTT_ERR_AGAIN 1837 | if err.errno == EAGAIN: 1838 | return MQTT_ERR_AGAIN 1839 | print(err) 1840 | return 1 1841 | else: 1842 | if len(data) == 0: 1843 | return 1 1844 | self._in_packet['to_process'] -= len(data) 1845 | self._in_packet['packet'] += data 1846 | 1847 | # All data for this packet is read. 1848 | self._in_packet['pos'] = 0 1849 | rc = self._packet_handle() 1850 | 1851 | # Free data and reset values 1852 | self._in_packet = { 1853 | 'command': 0, 1854 | 'have_remaining': 0, 1855 | 'remaining_count': [], 1856 | 'remaining_mult': 1, 1857 | 'remaining_length': 0, 1858 | 'packet': b"", 1859 | 'to_process': 0, 1860 | 'pos': 0} 1861 | 1862 | with self._msgtime_mutex: 1863 | self._last_msg_in = time_func() 1864 | return rc 1865 | 1866 | def _packet_write(self): 1867 | self._current_out_packet_mutex.acquire() 1868 | 1869 | while self._current_out_packet: 1870 | packet = self._current_out_packet 1871 | 1872 | try: 1873 | write_length = self._sock.send(packet['packet'][packet['pos']:]) 1874 | except (AttributeError, ValueError): 1875 | self._current_out_packet_mutex.release() 1876 | return MQTT_ERR_SUCCESS 1877 | except socket.error as err: 1878 | self._current_out_packet_mutex.release() 1879 | if self._ssl and (err.errno == ssl.SSL_ERROR_WANT_READ or err.errno == ssl.SSL_ERROR_WANT_WRITE): 1880 | return MQTT_ERR_AGAIN 1881 | if err.errno == EAGAIN: 1882 | return MQTT_ERR_AGAIN 1883 | print(err) 1884 | return 1 1885 | 1886 | if write_length > 0: 1887 | packet['to_process'] -= write_length 1888 | packet['pos'] += write_length 1889 | 1890 | if packet['to_process'] == 0: 1891 | if (packet['command'] & 0xF0) == PUBLISH and packet['qos'] == 0: 1892 | with self._callback_mutex: 1893 | if self.on_publish: 1894 | with self._in_callback: 1895 | self.on_publish(self, self._userdata, packet['mid']) 1896 | 1897 | packet['info']._set_as_published() 1898 | 1899 | if (packet['command'] & 0xF0) == DISCONNECT: 1900 | self._current_out_packet_mutex.release() 1901 | 1902 | with self._msgtime_mutex: 1903 | self._last_msg_out = time_func() 1904 | 1905 | with self._callback_mutex: 1906 | if self.on_disconnect: 1907 | with self._in_callback: 1908 | self.on_disconnect(self, self._userdata, 0) 1909 | 1910 | if self._sock: 1911 | self._sock.close() 1912 | self._sock = None 1913 | return MQTT_ERR_SUCCESS 1914 | 1915 | with self._out_packet_mutex: 1916 | if len(self._out_packet) > 0: 1917 | self._current_out_packet = self._out_packet.popleft() 1918 | else: 1919 | self._current_out_packet = None 1920 | else: 1921 | break 1922 | 1923 | self._current_out_packet_mutex.release() 1924 | 1925 | with self._msgtime_mutex: 1926 | self._last_msg_out = time_func() 1927 | 1928 | return MQTT_ERR_SUCCESS 1929 | 1930 | def _easy_log(self, level, fmt, *args): 1931 | if self.on_log: 1932 | buf = fmt % args 1933 | self.on_log(self, self._userdata, level, buf) 1934 | if self._logger: 1935 | level_std = LOGGING_LEVEL[level] 1936 | self._logger.log(level_std, fmt, *args) 1937 | 1938 | def _check_keepalive(self): 1939 | if self._keepalive == 0: 1940 | return MQTT_ERR_SUCCESS 1941 | 1942 | now = time_func() 1943 | 1944 | with self._msgtime_mutex: 1945 | last_msg_out = self._last_msg_out 1946 | last_msg_in = self._last_msg_in 1947 | 1948 | if self._sock is not None and (now - last_msg_out >= self._keepalive or now - last_msg_in >= self._keepalive): 1949 | if self._state == mqtt_cs_connected and self._ping_t == 0: 1950 | self._send_pingreq() 1951 | with self._msgtime_mutex: 1952 | self._last_msg_out = now 1953 | self._last_msg_in = now 1954 | else: 1955 | if self._sock: 1956 | self._sock.close() 1957 | self._sock = None 1958 | 1959 | if self._state == mqtt_cs_disconnecting: 1960 | rc = MQTT_ERR_SUCCESS 1961 | else: 1962 | rc = 1 1963 | with self._callback_mutex: 1964 | if self.on_disconnect: 1965 | with self._in_callback: 1966 | self.on_disconnect(self, self._userdata, rc) 1967 | 1968 | def _mid_generate(self): 1969 | self._last_mid += 1 1970 | if self._last_mid == 65536: 1971 | self._last_mid = 1 1972 | return self._last_mid 1973 | 1974 | @staticmethod 1975 | def _topic_wildcard_len_check(topic): 1976 | # Search for + or # in a topic. Return MQTT_ERR_INVAL if found. 1977 | # Also returns MQTT_ERR_INVAL if the topic string is too long. 1978 | # Returns MQTT_ERR_SUCCESS if everything is fine. 1979 | if b'+' in topic or b'#' in topic or len(topic) == 0 or len(topic) > 65535: 1980 | return MQTT_ERR_INVAL 1981 | else: 1982 | return MQTT_ERR_SUCCESS 1983 | 1984 | @staticmethod 1985 | def _filter_wildcard_len_check(sub): 1986 | if (len(sub) == 0 or len(sub) > 65535 1987 | or any(b'+' in p or b'#' in p for p in sub.split(b'/') if len(p) > 1) 1988 | or b'#/' in sub): 1989 | return MQTT_ERR_INVAL 1990 | else: 1991 | return MQTT_ERR_SUCCESS 1992 | 1993 | def _send_pingreq(self): 1994 | self._easy_log(MQTT_LOG_DEBUG, "Sending PINGREQ") 1995 | rc = self._send_simple_command(PINGREQ) 1996 | if rc == MQTT_ERR_SUCCESS: 1997 | self._ping_t = time_func() 1998 | return rc 1999 | 2000 | def _send_pingresp(self): 2001 | self._easy_log(MQTT_LOG_DEBUG, "Sending PINGRESP") 2002 | return self._send_simple_command(PINGRESP) 2003 | 2004 | def _send_puback(self, mid): 2005 | self._easy_log(MQTT_LOG_DEBUG, "Sending PUBACK (Mid: %d)", mid) 2006 | return self._send_command_with_mid(PUBACK, mid, False) 2007 | 2008 | def _send_pubcomp(self, mid): 2009 | self._easy_log(MQTT_LOG_DEBUG, "Sending PUBCOMP (Mid: %d)", mid) 2010 | return self._send_command_with_mid(PUBCOMP, mid, False) 2011 | 2012 | def _pack_remaining_length(self, packet, remaining_length): 2013 | remaining_bytes = [] 2014 | while True: 2015 | byte = remaining_length % 128 2016 | remaining_length = remaining_length // 128 2017 | # If there are more digits to encode, set the top bit of this digit 2018 | if remaining_length > 0: 2019 | byte |= 0x80 2020 | 2021 | remaining_bytes.append(byte) 2022 | packet.append(byte) 2023 | if remaining_length == 0: 2024 | # FIXME - this doesn't deal with incorrectly large payloads 2025 | return packet 2026 | 2027 | def _pack_str16(self, packet, data): 2028 | if isinstance(data, unicode): 2029 | data = data.encode('utf-8') 2030 | packet.extend(struct.pack("!H", len(data))) 2031 | packet.extend(data) 2032 | 2033 | def _send_publish(self, mid, topic, payload=b'', qos=0, retain=False, dup=False, info=None): 2034 | # we assume that topic and payload are already properly encoded 2035 | assert not isinstance(topic, unicode) and not isinstance(payload, unicode) and payload is not None 2036 | 2037 | if self._sock is None: 2038 | return MQTT_ERR_NO_CONN 2039 | 2040 | command = PUBLISH | ((dup & 0x1) << 3) | (qos << 1) | retain 2041 | packet = bytearray() 2042 | packet.append(command) 2043 | 2044 | payloadlen = len(payload) 2045 | remaining_length = 2 + len(topic) + payloadlen 2046 | 2047 | if payloadlen == 0: 2048 | self._easy_log( 2049 | MQTT_LOG_DEBUG, 2050 | "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s' (NULL payload)", 2051 | dup, qos, retain, mid, topic 2052 | ) 2053 | else: 2054 | self._easy_log( 2055 | MQTT_LOG_DEBUG, 2056 | "Sending PUBLISH (d%d, q%d, r%d, m%d), '%s', ... (%d bytes)", 2057 | dup, qos, retain, mid, topic, payloadlen 2058 | ) 2059 | 2060 | if qos > 0: 2061 | # For message id 2062 | remaining_length += 2 2063 | 2064 | self._pack_remaining_length(packet, remaining_length) 2065 | self._pack_str16(packet, topic) 2066 | 2067 | if qos > 0: 2068 | # For message id 2069 | packet.extend(struct.pack("!H", mid)) 2070 | 2071 | packet.extend(payload) 2072 | 2073 | return self._packet_queue(PUBLISH, packet, mid, qos, info) 2074 | 2075 | def _send_pubrec(self, mid): 2076 | self._easy_log(MQTT_LOG_DEBUG, "Sending PUBREC (Mid: %d)", mid) 2077 | return self._send_command_with_mid(PUBREC, mid, False) 2078 | 2079 | def _send_pubrel(self, mid, dup=False): 2080 | self._easy_log(MQTT_LOG_DEBUG, "Sending PUBREL (Mid: %d)", mid) 2081 | return self._send_command_with_mid(PUBREL | 2, mid, dup) 2082 | 2083 | def _send_command_with_mid(self, command, mid, dup): 2084 | # For PUBACK, PUBCOMP, PUBREC, and PUBREL 2085 | if dup: 2086 | command |= 0x8 2087 | 2088 | remaining_length = 2 2089 | packet = struct.pack('!BBH', command, remaining_length, mid) 2090 | return self._packet_queue(command, packet, mid, 1) 2091 | 2092 | def _send_simple_command(self, command): 2093 | # For DISCONNECT, PINGREQ and PINGRESP 2094 | remaining_length = 0 2095 | packet = struct.pack('!BB', command, remaining_length) 2096 | return self._packet_queue(command, packet, 0, 0) 2097 | 2098 | def _send_connect(self, keepalive, clean_session): 2099 | proto_ver = self._protocol 2100 | protocol = b"MQTT" if proto_ver >= MQTTv311 else b"MQIsdp" # hard-coded UTF-8 encoded string 2101 | 2102 | remaining_length = 2 + len(protocol) + 1 + 1 + 2 + 2 + len(self._client_id) 2103 | 2104 | connect_flags = 0 2105 | if clean_session: 2106 | connect_flags |= 0x02 2107 | 2108 | if self._will: 2109 | remaining_length += 2 + len(self._will_topic) + 2 + len(self._will_payload) 2110 | connect_flags |= 0x04 | ((self._will_qos & 0x03) << 3) | ((self._will_retain & 0x01) << 5) 2111 | 2112 | if self._username is not None: 2113 | remaining_length += 2 + len(self._username) 2114 | connect_flags |= 0x80 2115 | if self._password is not None: 2116 | connect_flags |= 0x40 2117 | remaining_length += 2 + len(self._password) 2118 | 2119 | command = CONNECT 2120 | packet = bytearray() 2121 | packet.append(command) 2122 | 2123 | self._pack_remaining_length(packet, remaining_length) 2124 | packet.extend(struct.pack("!H" + str(len(protocol)) + "sBBH", len(protocol), protocol, proto_ver, connect_flags, 2125 | keepalive)) 2126 | 2127 | self._pack_str16(packet, self._client_id) 2128 | 2129 | if self._will: 2130 | self._pack_str16(packet, self._will_topic) 2131 | self._pack_str16(packet, self._will_payload) 2132 | 2133 | if self._username is not None: 2134 | self._pack_str16(packet, self._username) 2135 | 2136 | if self._password is not None: 2137 | self._pack_str16(packet, self._password) 2138 | 2139 | self._keepalive = keepalive 2140 | self._easy_log( 2141 | MQTT_LOG_DEBUG, 2142 | "Sending CONNECT (u%d, p%d, wr%d, wq%d, wf%d, c%d, k%d) client_id=%s", 2143 | (connect_flags & 0x80) >> 7, 2144 | (connect_flags & 0x40) >> 6, 2145 | (connect_flags & 0x20) >> 5, 2146 | (connect_flags & 0x18) >> 3, 2147 | (connect_flags & 0x4) >> 2, 2148 | (connect_flags & 0x2) >> 1, 2149 | keepalive, 2150 | self._client_id 2151 | ) 2152 | return self._packet_queue(command, packet, 0, 0) 2153 | 2154 | def _send_disconnect(self): 2155 | self._easy_log(MQTT_LOG_DEBUG, "Sending DISCONNECT") 2156 | return self._send_simple_command(DISCONNECT) 2157 | 2158 | def _send_subscribe(self, dup, topics): 2159 | remaining_length = 2 2160 | for t, _ in topics: 2161 | remaining_length += 2 + len(t) + 1 2162 | 2163 | command = SUBSCRIBE | (dup << 3) | 0x2 2164 | packet = bytearray() 2165 | packet.append(command) 2166 | self._pack_remaining_length(packet, remaining_length) 2167 | local_mid = self._mid_generate() 2168 | packet.extend(struct.pack("!H", local_mid)) 2169 | for t, q in topics: 2170 | self._pack_str16(packet, t) 2171 | packet.append(q) 2172 | 2173 | self._easy_log(MQTT_LOG_DEBUG, "Sending SUBSCRIBE (d%d) %s", dup, topics) 2174 | return (self._packet_queue(command, packet, local_mid, 1), local_mid) 2175 | 2176 | def _send_unsubscribe(self, dup, topics): 2177 | remaining_length = 2 2178 | for t in topics: 2179 | remaining_length += 2 + len(t) 2180 | 2181 | command = UNSUBSCRIBE | (dup << 3) | 0x2 2182 | packet = bytearray() 2183 | packet.append(command) 2184 | self._pack_remaining_length(packet, remaining_length) 2185 | local_mid = self._mid_generate() 2186 | packet.extend(struct.pack("!H", local_mid)) 2187 | for t in topics: 2188 | self._pack_str16(packet, t) 2189 | 2190 | # topics_repr = ", ".join("'"+topic.decode('utf8')+"'" for topic in topics) 2191 | self._easy_log(MQTT_LOG_DEBUG, "Sending UNSUBSCRIBE (d%d) %s", dup, topics) 2192 | return (self._packet_queue(command, packet, local_mid, 1), local_mid) 2193 | 2194 | def _message_retry_check_actual(self, messages, mutex): 2195 | with mutex: 2196 | now = time_func() 2197 | for m in messages: 2198 | if m.timestamp + self._message_retry < now: 2199 | if m.state == mqtt_ms_wait_for_puback or m.state == mqtt_ms_wait_for_pubrec: 2200 | m.timestamp = now 2201 | m.dup = True 2202 | self._send_publish( 2203 | m.mid, 2204 | m.topic.encode('utf-8'), 2205 | m.payload, 2206 | m.qos, 2207 | m.retain, 2208 | m.dup 2209 | ) 2210 | elif m.state == mqtt_ms_wait_for_pubrel: 2211 | m.timestamp = now 2212 | m.dup = True 2213 | self._send_pubrec(m.mid) 2214 | elif m.state == mqtt_ms_wait_for_pubcomp: 2215 | m.timestamp = now 2216 | m.dup = True 2217 | self._send_pubrel(m.mid, True) 2218 | 2219 | def _message_retry_check(self): 2220 | self._message_retry_check_actual(self._out_messages, self._out_message_mutex) 2221 | self._message_retry_check_actual(self._in_messages, self._in_message_mutex) 2222 | 2223 | def _messages_reconnect_reset_out(self): 2224 | with self._out_message_mutex: 2225 | self._inflight_messages = 0 2226 | for m in self._out_messages: 2227 | m.timestamp = 0 2228 | if self._max_inflight_messages == 0 or self._inflight_messages < self._max_inflight_messages: 2229 | if m.qos == 0: 2230 | m.state = mqtt_ms_publish 2231 | elif m.qos == 1: 2232 | # self._inflight_messages = self._inflight_messages + 1 2233 | if m.state == mqtt_ms_wait_for_puback: 2234 | m.dup = True 2235 | m.state = mqtt_ms_publish 2236 | elif m.qos == 2: 2237 | # self._inflight_messages = self._inflight_messages + 1 2238 | if m.state == mqtt_ms_wait_for_pubcomp: 2239 | m.state = mqtt_ms_resend_pubrel 2240 | m.dup = True 2241 | else: 2242 | if m.state == mqtt_ms_wait_for_pubrec: 2243 | m.dup = True 2244 | m.state = mqtt_ms_publish 2245 | else: 2246 | m.state = mqtt_ms_queued 2247 | 2248 | def _messages_reconnect_reset_in(self): 2249 | with self._in_message_mutex: 2250 | for m in self._in_messages: 2251 | m.timestamp = 0 2252 | if m.qos != 2: 2253 | self._in_messages.pop(self._in_messages.index(m)) 2254 | else: 2255 | # Preserve current state 2256 | pass 2257 | 2258 | def _messages_reconnect_reset(self): 2259 | self._messages_reconnect_reset_out() 2260 | self._messages_reconnect_reset_in() 2261 | 2262 | def _packet_queue(self, command, packet, mid, qos, info=None): 2263 | mpkt = { 2264 | 'command': command, 2265 | 'mid': mid, 2266 | 'qos': qos, 2267 | 'pos': 0, 2268 | 'to_process': len(packet), 2269 | 'packet': packet, 2270 | 'info': info} 2271 | 2272 | with self._out_packet_mutex: 2273 | self._out_packet.append(mpkt) 2274 | if self._current_out_packet_mutex.acquire(False): 2275 | if self._current_out_packet is None and len(self._out_packet) > 0: 2276 | self._current_out_packet = self._out_packet.popleft() 2277 | self._current_out_packet_mutex.release() 2278 | 2279 | # Write a single byte to sockpairW (connected to sockpairR) to break 2280 | # out of select() if in threaded mode. 2281 | try: 2282 | self._sockpairW.send(sockpair_data) 2283 | except socket.error as err: 2284 | if err.errno != EAGAIN: 2285 | raise 2286 | 2287 | if self._thread is None: 2288 | if self._in_callback.acquire(False): 2289 | self._in_callback.release() 2290 | return self.loop_write() 2291 | 2292 | return MQTT_ERR_SUCCESS 2293 | 2294 | def _packet_handle(self): 2295 | cmd = self._in_packet['command'] & 0xF0 2296 | if cmd == PINGREQ: 2297 | return self._handle_pingreq() 2298 | elif cmd == PINGRESP: 2299 | return self._handle_pingresp() 2300 | elif cmd == PUBACK: 2301 | return self._handle_pubackcomp("PUBACK") 2302 | elif cmd == PUBCOMP: 2303 | return self._handle_pubackcomp("PUBCOMP") 2304 | elif cmd == PUBLISH: 2305 | return self._handle_publish() 2306 | elif cmd == PUBREC: 2307 | return self._handle_pubrec() 2308 | elif cmd == PUBREL: 2309 | return self._handle_pubrel() 2310 | elif cmd == CONNACK: 2311 | return self._handle_connack() 2312 | elif cmd == SUBACK: 2313 | return self._handle_suback() 2314 | elif cmd == UNSUBACK: 2315 | return self._handle_unsuback() 2316 | else: 2317 | # If we don't recognise the command, return an error straight away. 2318 | self._easy_log(MQTT_LOG_ERR, "Error: Unrecognised command %s", cmd) 2319 | return MQTT_ERR_PROTOCOL 2320 | 2321 | def _handle_pingreq(self): 2322 | if self._in_packet['remaining_length'] != 0: 2323 | return MQTT_ERR_PROTOCOL 2324 | 2325 | self._easy_log(MQTT_LOG_DEBUG, "Received PINGREQ") 2326 | return self._send_pingresp() 2327 | 2328 | def _handle_pingresp(self): 2329 | if self._in_packet['remaining_length'] != 0: 2330 | return MQTT_ERR_PROTOCOL 2331 | 2332 | # No longer waiting for a PINGRESP. 2333 | self._ping_t = 0 2334 | self._easy_log(MQTT_LOG_DEBUG, "Received PINGRESP") 2335 | return MQTT_ERR_SUCCESS 2336 | 2337 | def _handle_connack(self): 2338 | if self._in_packet['remaining_length'] != 2: 2339 | return MQTT_ERR_PROTOCOL 2340 | 2341 | (flags, result) = struct.unpack("!BB", self._in_packet['packet']) 2342 | if result == CONNACK_REFUSED_PROTOCOL_VERSION and self._protocol == MQTTv311: 2343 | self._easy_log( 2344 | MQTT_LOG_DEBUG, 2345 | "Received CONNACK (%s, %s), attempting downgrade to MQTT v3.1.", 2346 | flags, result 2347 | ) 2348 | # Downgrade to MQTT v3.1 2349 | self._protocol = MQTTv31 2350 | return self.reconnect() 2351 | elif (result == CONNACK_REFUSED_IDENTIFIER_REJECTED 2352 | and self._client_id == b''): 2353 | self._easy_log( 2354 | MQTT_LOG_DEBUG, 2355 | "Received CONNACK (%s, %s), attempting to use non-empty CID", 2356 | flags, result, 2357 | ) 2358 | self._client_id = base62(uuid.uuid4().int, padding=22) 2359 | return self.reconnect() 2360 | 2361 | if result == 0: 2362 | self._state = mqtt_cs_connected 2363 | self._reconnect_delay = None 2364 | 2365 | self._easy_log(MQTT_LOG_DEBUG, "Received CONNACK (%s, %s)", flags, result) 2366 | 2367 | with self._callback_mutex: 2368 | if self.on_connect: 2369 | flags_dict = {} 2370 | flags_dict['session present'] = flags & 0x01 2371 | with self._in_callback: 2372 | self.on_connect(self, self._userdata, flags_dict, result) 2373 | 2374 | if result == 0: 2375 | rc = 0 2376 | with self._out_message_mutex: 2377 | for m in self._out_messages: 2378 | m.timestamp = time_func() 2379 | if m.state == mqtt_ms_queued: 2380 | self.loop_write() # Process outgoing messages that have just been queued up 2381 | return MQTT_ERR_SUCCESS 2382 | 2383 | if m.qos == 0: 2384 | with self._in_callback: # Don't call loop_write after _send_publish() 2385 | rc = self._send_publish( 2386 | m.mid, 2387 | m.topic.encode('utf-8'), 2388 | m.payload, 2389 | m.qos, 2390 | m.retain, 2391 | m.dup, 2392 | ) 2393 | if rc != 0: 2394 | return rc 2395 | elif m.qos == 1: 2396 | if m.state == mqtt_ms_publish: 2397 | self._inflight_messages += 1 2398 | m.state = mqtt_ms_wait_for_puback 2399 | with self._in_callback: # Don't call loop_write after _send_publish() 2400 | rc = self._send_publish( 2401 | m.mid, 2402 | m.topic.encode('utf-8'), 2403 | m.payload, 2404 | m.qos, 2405 | m.retain, 2406 | m.dup, 2407 | ) 2408 | if rc != 0: 2409 | return rc 2410 | elif m.qos == 2: 2411 | if m.state == mqtt_ms_publish: 2412 | self._inflight_messages += 1 2413 | m.state = mqtt_ms_wait_for_pubrec 2414 | with self._in_callback: # Don't call loop_write after _send_publish() 2415 | rc = self._send_publish( 2416 | m.mid, 2417 | m.topic.encode('utf-8'), 2418 | m.payload, 2419 | m.qos, 2420 | m.retain, 2421 | m.dup, 2422 | ) 2423 | if rc != 0: 2424 | return rc 2425 | elif m.state == mqtt_ms_resend_pubrel: 2426 | self._inflight_messages += 1 2427 | m.state = mqtt_ms_wait_for_pubcomp 2428 | with self._in_callback: # Don't call loop_write after _send_publish() 2429 | rc = self._send_pubrel(m.mid, m.dup) 2430 | if rc != 0: 2431 | return rc 2432 | self.loop_write() # Process outgoing messages that have just been queued up 2433 | 2434 | return rc 2435 | elif result > 0 and result < 6: 2436 | return MQTT_ERR_CONN_REFUSED 2437 | else: 2438 | return MQTT_ERR_PROTOCOL 2439 | 2440 | def _handle_suback(self): 2441 | self._easy_log(MQTT_LOG_DEBUG, "Received SUBACK") 2442 | pack_format = "!H" + str(len(self._in_packet['packet']) - 2) + 's' 2443 | (mid, packet) = struct.unpack(pack_format, self._in_packet['packet']) 2444 | pack_format = "!" + "B" * len(packet) 2445 | granted_qos = struct.unpack(pack_format, packet) 2446 | 2447 | with self._callback_mutex: 2448 | if self.on_subscribe: 2449 | with self._in_callback: # Don't call loop_write after _send_publish() 2450 | self.on_subscribe(self, self._userdata, mid, granted_qos) 2451 | 2452 | return MQTT_ERR_SUCCESS 2453 | 2454 | def _handle_publish(self): 2455 | rc = 0 2456 | 2457 | header = self._in_packet['command'] 2458 | message = MQTTMessage() 2459 | message.dup = (header & 0x08) >> 3 2460 | message.qos = (header & 0x06) >> 1 2461 | message.retain = (header & 0x01) 2462 | 2463 | pack_format = "!H" + str(len(self._in_packet['packet']) - 2) + 's' 2464 | (slen, packet) = struct.unpack(pack_format, self._in_packet['packet']) 2465 | pack_format = '!' + str(slen) + 's' + str(len(packet) - slen) + 's' 2466 | (topic, packet) = struct.unpack(pack_format, packet) 2467 | 2468 | if len(topic) == 0: 2469 | return MQTT_ERR_PROTOCOL 2470 | 2471 | # Handle topics with invalid UTF-8 2472 | # This replaces an invalid topic with a message and the hex 2473 | # representation of the topic for logging. When the user attempts to 2474 | # access message.topic in the callback, an exception will be raised. 2475 | if sys.version_info[0] >= 3: 2476 | try: 2477 | print_topic = topic.decode('utf-8') 2478 | except UnicodeDecodeError: 2479 | print_topic = "TOPIC WITH INVALID UTF-8: " + str(topic) 2480 | else: 2481 | print_topic = topic 2482 | 2483 | message.topic = topic 2484 | 2485 | if message.qos > 0: 2486 | pack_format = "!H" + str(len(packet) - 2) + 's' 2487 | (message.mid, packet) = struct.unpack(pack_format, packet) 2488 | 2489 | message.payload = packet 2490 | 2491 | self._easy_log( 2492 | MQTT_LOG_DEBUG, 2493 | "Received PUBLISH (d%d, q%d, r%d, m%d), '%s', ... (%d bytes)", 2494 | message.dup, message.qos, message.retain, message.mid, 2495 | print_topic, len(message.payload) 2496 | ) 2497 | 2498 | message.timestamp = time_func() 2499 | if message.qos == 0: 2500 | self._handle_on_message(message) 2501 | return MQTT_ERR_SUCCESS 2502 | elif message.qos == 1: 2503 | rc = self._send_puback(message.mid) 2504 | self._handle_on_message(message) 2505 | return rc 2506 | elif message.qos == 2: 2507 | rc = self._send_pubrec(message.mid) 2508 | message.state = mqtt_ms_wait_for_pubrel 2509 | with self._in_message_mutex: 2510 | if message not in self._in_messages: 2511 | self._in_messages.append(message) 2512 | return rc 2513 | else: 2514 | return MQTT_ERR_PROTOCOL 2515 | 2516 | def _handle_pubrel(self): 2517 | if self._in_packet['remaining_length'] != 2: 2518 | return MQTT_ERR_PROTOCOL 2519 | 2520 | mid, = struct.unpack("!H", self._in_packet['packet']) 2521 | self._easy_log(MQTT_LOG_DEBUG, "Received PUBREL (Mid: %d)", mid) 2522 | 2523 | with self._in_message_mutex: 2524 | for i in range(len(self._in_messages)): 2525 | if self._in_messages[i].mid == mid: 2526 | 2527 | # Only pass the message on if we have removed it from the queue - this 2528 | # prevents multiple callbacks for the same message. 2529 | self._handle_on_message(self._in_messages[i]) 2530 | self._in_messages.pop(i) 2531 | self._inflight_messages -= 1 2532 | if self._max_inflight_messages > 0: 2533 | with self._out_message_mutex: 2534 | rc = self._update_inflight() 2535 | if rc != MQTT_ERR_SUCCESS: 2536 | return rc 2537 | 2538 | return self._send_pubcomp(mid) 2539 | 2540 | return MQTT_ERR_SUCCESS 2541 | 2542 | def _update_inflight(self): 2543 | # Dont lock message_mutex here 2544 | for m in self._out_messages: 2545 | if self._inflight_messages < self._max_inflight_messages: 2546 | if m.qos > 0 and m.state == mqtt_ms_queued: 2547 | self._inflight_messages += 1 2548 | if m.qos == 1: 2549 | m.state = mqtt_ms_wait_for_puback 2550 | elif m.qos == 2: 2551 | m.state = mqtt_ms_wait_for_pubrec 2552 | rc = self._send_publish( 2553 | m.mid, 2554 | m.topic.encode('utf-8'), 2555 | m.payload, 2556 | m.qos, 2557 | m.retain, 2558 | m.dup, 2559 | ) 2560 | if rc != 0: 2561 | return rc 2562 | else: 2563 | return MQTT_ERR_SUCCESS 2564 | return MQTT_ERR_SUCCESS 2565 | 2566 | def _handle_pubrec(self): 2567 | if self._in_packet['remaining_length'] != 2: 2568 | return MQTT_ERR_PROTOCOL 2569 | 2570 | mid, = struct.unpack("!H", self._in_packet['packet']) 2571 | self._easy_log(MQTT_LOG_DEBUG, "Received PUBREC (Mid: %d)", mid) 2572 | 2573 | with self._out_message_mutex: 2574 | for m in self._out_messages: 2575 | if m.mid == mid: 2576 | m.state = mqtt_ms_wait_for_pubcomp 2577 | m.timestamp = time_func() 2578 | return self._send_pubrel(mid, False) 2579 | 2580 | return MQTT_ERR_SUCCESS 2581 | 2582 | def _handle_unsuback(self): 2583 | if self._in_packet['remaining_length'] != 2: 2584 | return MQTT_ERR_PROTOCOL 2585 | 2586 | mid, = struct.unpack("!H", self._in_packet['packet']) 2587 | self._easy_log(MQTT_LOG_DEBUG, "Received UNSUBACK (Mid: %d)", mid) 2588 | with self._callback_mutex: 2589 | if self.on_unsubscribe: 2590 | with self._in_callback: 2591 | self.on_unsubscribe(self, self._userdata, mid) 2592 | return MQTT_ERR_SUCCESS 2593 | 2594 | def _do_on_publish(self, idx, mid): 2595 | with self._callback_mutex: 2596 | if self.on_publish: 2597 | with self._in_callback: 2598 | self.on_publish(self, self._userdata, mid) 2599 | 2600 | msg = self._out_messages.pop(idx) 2601 | if msg.qos > 0: 2602 | self._inflight_messages -= 1 2603 | if self._max_inflight_messages > 0: 2604 | rc = self._update_inflight() 2605 | if rc != MQTT_ERR_SUCCESS: 2606 | return rc 2607 | msg.info._set_as_published() 2608 | return MQTT_ERR_SUCCESS 2609 | 2610 | def _handle_pubackcomp(self, cmd): 2611 | if self._in_packet['remaining_length'] != 2: 2612 | return MQTT_ERR_PROTOCOL 2613 | 2614 | mid, = struct.unpack("!H", self._in_packet['packet']) 2615 | self._easy_log(MQTT_LOG_DEBUG, "Received %s (Mid: %d)", cmd, mid) 2616 | 2617 | with self._out_message_mutex: 2618 | for i in range(len(self._out_messages)): 2619 | try: 2620 | if self._out_messages[i].mid == mid: 2621 | # Only inform the client the message has been sent once. 2622 | rc = self._do_on_publish(i, mid) 2623 | return rc 2624 | except IndexError: 2625 | # Have removed item so i>count. 2626 | # Not really an error. 2627 | pass 2628 | 2629 | return MQTT_ERR_SUCCESS 2630 | 2631 | def _handle_on_message(self, message): 2632 | matched = False 2633 | with self._callback_mutex: 2634 | try: 2635 | topic = message.topic 2636 | except UnicodeDecodeError: 2637 | topic = None 2638 | 2639 | if topic is not None: 2640 | for callback in self._on_message_filtered.iter_match(message.topic): 2641 | with self._in_callback: 2642 | callback(self, self._userdata, message) 2643 | matched = True 2644 | 2645 | if matched == False and self.on_message: 2646 | with self._in_callback: 2647 | self.on_message(self, self._userdata, message) 2648 | 2649 | def _thread_main(self): 2650 | self.loop_forever(retry_first_connection=True) 2651 | 2652 | def _reconnect_wait(self): 2653 | # See reconnect_delay_set for details 2654 | now = time_func() 2655 | with self._reconnect_delay_mutex: 2656 | if self._reconnect_delay is None: 2657 | self._reconnect_delay = self._reconnect_min_delay 2658 | else: 2659 | self._reconnect_delay = min( 2660 | self._reconnect_delay * 2, 2661 | self._reconnect_max_delay, 2662 | ) 2663 | 2664 | target_time = now + self._reconnect_delay 2665 | 2666 | remaining = target_time - now 2667 | while (self._state != mqtt_cs_disconnecting 2668 | and not self._thread_terminate 2669 | and remaining > 0): 2670 | 2671 | time.sleep(min(remaining, 1)) 2672 | remaining = target_time - time_func() 2673 | 2674 | 2675 | # Compatibility class for easy porting from mosquitto.py. 2676 | class Mosquitto(Client): 2677 | def __init__(self, client_id="", clean_session=True, userdata=None): 2678 | super(Mosquitto, self).__init__(client_id, clean_session, userdata) 2679 | 2680 | 2681 | class WebsocketWrapper(object): 2682 | OPCODE_CONTINUATION = 0x0 2683 | OPCODE_TEXT = 0x1 2684 | OPCODE_BINARY = 0x2 2685 | OPCODE_CONNCLOSE = 0x8 2686 | OPCODE_PING = 0x9 2687 | OPCODE_PONG = 0xa 2688 | 2689 | def __init__(self, socket, host, port, is_ssl, path, extra_headers): 2690 | 2691 | self.connected = False 2692 | 2693 | self._ssl = is_ssl 2694 | self._host = host 2695 | self._port = port 2696 | self._socket = socket 2697 | self._path = path 2698 | 2699 | self._sendbuffer = bytearray() 2700 | self._readbuffer = bytearray() 2701 | 2702 | self._requested_size = 0 2703 | self._payload_head = 0 2704 | self._readbuffer_head = 0 2705 | 2706 | self._do_handshake(extra_headers) 2707 | 2708 | def __del__(self): 2709 | 2710 | self._sendbuffer = None 2711 | self._readbuffer = None 2712 | 2713 | def _do_handshake(self, extra_headers): 2714 | 2715 | sec_websocket_key = uuid.uuid4().bytes 2716 | sec_websocket_key = base64.b64encode(sec_websocket_key) 2717 | 2718 | websocket_headers = { 2719 | "Host": "{self._host:s}:{self._port:d}".format(self=self), 2720 | "Upgrade": "websocket", 2721 | "Connection": "Upgrade", 2722 | "Origin": "https://{self._host:s}:{self._port:d}".format(self=self), 2723 | "Sec-WebSocket-Key": sec_websocket_key.decode("utf8"), 2724 | "Sec-Websocket-Version": "13", 2725 | "Sec-Websocket-Protocol": "mqtt", 2726 | } 2727 | 2728 | # This is checked in ws_set_options so it will either be None, a 2729 | # dictionary, or a callable 2730 | if isinstance(extra_headers, dict): 2731 | websocket_headers.update(extra_headers) 2732 | elif callable(extra_headers): 2733 | websocket_headers = extra_headers(websocket_headers) 2734 | 2735 | header = "\r\n".join([ 2736 | "GET {self._path} HTTP/1.1".format(self=self), 2737 | "\r\n".join("{}: {}".format(i, j) for i, j in websocket_headers.items()), 2738 | "\r\n", 2739 | ]).encode("utf8") 2740 | 2741 | self._socket.send(header) 2742 | 2743 | has_secret = False 2744 | has_upgrade = False 2745 | 2746 | while True: 2747 | # read HTTP response header as lines 2748 | byte = self._socket.recv(1) 2749 | 2750 | self._readbuffer.extend(byte) 2751 | 2752 | # line end 2753 | if byte == b"\n": 2754 | if len(self._readbuffer) > 2: 2755 | # check upgrade 2756 | if b"connection" in str(self._readbuffer).lower().encode('utf-8'): 2757 | if b"upgrade" not in str(self._readbuffer).lower().encode('utf-8'): 2758 | raise WebsocketConnectionError("WebSocket handshake error, connection not upgraded") 2759 | else: 2760 | has_upgrade = True 2761 | 2762 | # check key hash 2763 | if b"sec-websocket-accept" in str(self._readbuffer).lower().encode('utf-8'): 2764 | GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 2765 | 2766 | server_hash = self._readbuffer.decode('utf-8').split(": ", 1)[1] 2767 | server_hash = server_hash.strip().encode('utf-8') 2768 | 2769 | client_hash = sec_websocket_key.decode('utf-8') + GUID 2770 | client_hash = hashlib.sha1(client_hash.encode('utf-8')) 2771 | client_hash = base64.b64encode(client_hash.digest()) 2772 | 2773 | if server_hash != client_hash: 2774 | raise WebsocketConnectionError("WebSocket handshake error, invalid secret key") 2775 | else: 2776 | has_secret = True 2777 | else: 2778 | # ending linebreak 2779 | break 2780 | 2781 | # reset linebuffer 2782 | self._readbuffer = bytearray() 2783 | 2784 | # connection reset 2785 | elif not byte: 2786 | raise WebsocketConnectionError("WebSocket handshake error") 2787 | 2788 | if not has_upgrade or not has_secret: 2789 | raise WebsocketConnectionError("WebSocket handshake error") 2790 | 2791 | self._readbuffer = bytearray() 2792 | self.connected = True 2793 | 2794 | def _create_frame(self, opcode, data, do_masking=1): 2795 | 2796 | header = bytearray() 2797 | length = len(data) 2798 | mask_key = bytearray( 2799 | [random.randint(0, 255), random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)]) 2800 | mask_flag = do_masking 2801 | 2802 | # 1 << 7 is the final flag, we don't send continuated data 2803 | header.append(1 << 7 | opcode) 2804 | 2805 | if length < 126: 2806 | header.append(mask_flag << 7 | length) 2807 | 2808 | elif length < 32768: 2809 | header.append(mask_flag << 7 | 126) 2810 | header += struct.pack("!H", length) 2811 | 2812 | elif length < 0x8000000000000001: 2813 | header.append(mask_flag << 7 | 127) 2814 | header += struct.pack("!Q", length) 2815 | 2816 | else: 2817 | raise ValueError("Maximum payload size is 2^63") 2818 | 2819 | if mask_flag == 1: 2820 | for index in range(length): 2821 | data[index] ^= mask_key[index % 4] 2822 | data = mask_key + data 2823 | 2824 | return header + data 2825 | 2826 | def _buffered_read(self, length): 2827 | 2828 | # try to recv and strore needed bytes 2829 | wanted_bytes = length - (len(self._readbuffer) - self._readbuffer_head) 2830 | if wanted_bytes > 0: 2831 | 2832 | data = self._socket.recv(wanted_bytes) 2833 | 2834 | if not data: 2835 | raise socket.error(errno.ECONNABORTED, 0) 2836 | else: 2837 | self._readbuffer.extend(data) 2838 | 2839 | if len(data) < wanted_bytes: 2840 | raise socket.error(errno.EAGAIN, 0) 2841 | 2842 | self._readbuffer_head += length 2843 | return self._readbuffer[self._readbuffer_head - length:self._readbuffer_head] 2844 | 2845 | def _recv_impl(self, length): 2846 | 2847 | # try to decode websocket payload part from data 2848 | try: 2849 | 2850 | self._readbuffer_head = 0 2851 | 2852 | result = None 2853 | 2854 | chunk_startindex = self._payload_head 2855 | chunk_endindex = self._payload_head + length 2856 | 2857 | header1 = self._buffered_read(1) 2858 | header2 = self._buffered_read(1) 2859 | 2860 | opcode = (header1[0] & 0x0f) 2861 | maskbit = (header2[0] & 0x80) == 0x80 2862 | lengthbits = (header2[0] & 0x7f) 2863 | payload_length = lengthbits 2864 | mask_key = None 2865 | 2866 | # read length 2867 | if lengthbits == 0x7e: 2868 | 2869 | value = self._buffered_read(2) 2870 | payload_length, = struct.unpack("!H", value) 2871 | 2872 | elif lengthbits == 0x7f: 2873 | 2874 | value = self._buffered_read(8) 2875 | payload_length, = struct.unpack("!Q", value) 2876 | 2877 | # read mask 2878 | if maskbit: 2879 | mask_key = self._buffered_read(4) 2880 | 2881 | # if frame payload is shorter than the requested data, read only the possible part 2882 | readindex = chunk_endindex 2883 | if payload_length < readindex: 2884 | readindex = payload_length 2885 | 2886 | if readindex > 0: 2887 | # get payload chunk 2888 | payload = self._buffered_read(readindex) 2889 | 2890 | # unmask only the needed part 2891 | if maskbit: 2892 | for index in range(chunk_startindex, readindex): 2893 | payload[index] ^= mask_key[index % 4] 2894 | 2895 | result = payload[chunk_startindex:readindex] 2896 | self._payload_head = readindex 2897 | else: 2898 | payload = bytearray() 2899 | 2900 | # check if full frame arrived and reset readbuffer and payloadhead if needed 2901 | if readindex == payload_length: 2902 | self._readbuffer = bytearray() 2903 | self._payload_head = 0 2904 | 2905 | # respond to non-binary opcodes, their arrival is not guaranteed beacause of non-blocking sockets 2906 | if opcode == WebsocketWrapper.OPCODE_CONNCLOSE: 2907 | frame = self._create_frame(WebsocketWrapper.OPCODE_CONNCLOSE, payload, 0) 2908 | self._socket.send(frame) 2909 | 2910 | if opcode == WebsocketWrapper.OPCODE_PING: 2911 | frame = self._create_frame(WebsocketWrapper.OPCODE_PONG, payload, 0) 2912 | self._socket.send(frame) 2913 | 2914 | if opcode == WebsocketWrapper.OPCODE_BINARY: 2915 | return result 2916 | else: 2917 | raise socket.error(errno.EAGAIN, 0) 2918 | 2919 | except socket.error as err: 2920 | 2921 | if err.errno == errno.ECONNABORTED: 2922 | self.connected = False 2923 | return b'' 2924 | else: 2925 | # no more data 2926 | raise 2927 | 2928 | def _send_impl(self, data): 2929 | 2930 | # if previous frame was sent successfully 2931 | if len(self._sendbuffer) == 0: 2932 | # create websocket frame 2933 | frame = self._create_frame(WebsocketWrapper.OPCODE_BINARY, bytearray(data)) 2934 | self._sendbuffer.extend(frame) 2935 | self._requested_size = len(data) 2936 | 2937 | # try to write out as much as possible 2938 | length = self._socket.send(self._sendbuffer) 2939 | 2940 | self._sendbuffer = self._sendbuffer[length:] 2941 | 2942 | if len(self._sendbuffer) == 0: 2943 | # buffer sent out completely, return with payload's size 2944 | return self._requested_size 2945 | else: 2946 | # couldn't send whole data, request the same data again with 0 as sent length 2947 | return 0 2948 | 2949 | def recv(self, length): 2950 | return self._recv_impl(length) 2951 | 2952 | def read(self, length): 2953 | return self._recv_impl(length) 2954 | 2955 | def send(self, data): 2956 | return self._send_impl(data) 2957 | 2958 | def write(self, data): 2959 | return self._send_impl(data) 2960 | 2961 | def close(self): 2962 | self._socket.close() 2963 | 2964 | def fileno(self): 2965 | return self._socket.fileno() 2966 | 2967 | def pending(self): 2968 | # Fix for bug #131: a SSL socket may still have data available 2969 | # for reading without select() being aware of it. 2970 | if self._ssl: 2971 | return self._socket.pending() 2972 | else: 2973 | # normal socket rely only on select() 2974 | return 0 2975 | 2976 | def setblocking(self, flag): 2977 | self._socket.setblocking(flag) 2978 | -------------------------------------------------------------------------------- /paho/mqtt/matcher.py: -------------------------------------------------------------------------------- 1 | class MQTTMatcher(object): 2 | """Intended to manage topic filters including wildcards. 3 | 4 | Internally, MQTTMatcher use a prefix tree (trie) to store 5 | values associated with filters, and has an iter_match() 6 | method to iterate efficiently over all filters that match 7 | some topic name.""" 8 | 9 | class Node(object): 10 | __slots__ = '_children', '_content' 11 | 12 | def __init__(self): 13 | self._children = {} 14 | self._content = None 15 | 16 | def __init__(self): 17 | self._root = self.Node() 18 | 19 | def __setitem__(self, key, value): 20 | """Add a topic filter :key to the prefix tree 21 | and associate it to :value""" 22 | node = self._root 23 | for sym in key.split('/'): 24 | node = node._children.setdefault(sym, self.Node()) 25 | node._content = value 26 | 27 | def __getitem__(self, key): 28 | """Retrieve the value associated with some topic filter :key""" 29 | try: 30 | node = self._root 31 | for sym in key.split('/'): 32 | node = node._children[sym] 33 | if node._content is None: 34 | raise KeyError(key) 35 | return node._content 36 | except KeyError: 37 | raise KeyError(key) 38 | 39 | def __delitem__(self, key): 40 | """Delete the value associated with some topic filter :key""" 41 | lst = [] 42 | try: 43 | parent, node = None, self._root 44 | for k in key.split('/'): 45 | parent, node = node, node._children[k] 46 | lst.append((parent, k, node)) 47 | # TODO 48 | node._content = None 49 | except KeyError: 50 | raise KeyError(key) 51 | else: # cleanup 52 | for parent, k, node in reversed(lst): 53 | if node._children or node._content is not None: 54 | break 55 | del parent._children[k] 56 | 57 | def iter_match(self, topic): 58 | """Return an iterator on all values associated with filters 59 | that match the :topic""" 60 | lst = topic.split('/') 61 | normal = not topic.startswith('$') 62 | def rec(node, i=0): 63 | if i == len(lst): 64 | if node._content is not None: 65 | yield node._content 66 | else: 67 | part = lst[i] 68 | if part in node._children: 69 | for content in rec(node._children[part], i + 1): 70 | yield content 71 | if '+' in node._children and (normal or i > 0): 72 | for content in rec(node._children['+'], i + 1): 73 | yield content 74 | if '#' in node._children and (normal or i > 0): 75 | content = node._children['#']._content 76 | if content is not None: 77 | yield content 78 | return rec(self._root) 79 | -------------------------------------------------------------------------------- /paho/mqtt/publish.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Roger Light 2 | # 3 | # All rights reserved. This program and the accompanying materials 4 | # are made available under the terms of the Eclipse Public License v1.0 5 | # and Eclipse Distribution License v1.0 which accompany this distribution. 6 | # 7 | # The Eclipse Public License is available at 8 | # http://www.eclipse.org/legal/epl-v10.html 9 | # and the Eclipse Distribution License is available at 10 | # http://www.eclipse.org/org/documents/edl-v10.php. 11 | # 12 | # Contributors: 13 | # Roger Light - initial API and implementation 14 | 15 | """ 16 | This module provides some helper functions to allow straightforward publishing 17 | of messages in a one-shot manner. In other words, they are useful for the 18 | situation where you have a single/multiple messages you want to publish to a 19 | broker, then disconnect and nothing else is required. 20 | """ 21 | 22 | import paho.mqtt.client as paho 23 | import paho.mqtt as mqtt 24 | 25 | 26 | def _do_publish(client): 27 | """Internal function""" 28 | 29 | message = client._userdata.pop() 30 | 31 | if isinstance(message, dict): 32 | client.publish(**message) 33 | elif isinstance(message, tuple): 34 | client.publish(*message) 35 | else: 36 | raise ValueError('message must be a dict or a tuple') 37 | 38 | 39 | def _on_connect(client, userdata, flags, rc): 40 | """Internal callback""" 41 | #pylint: disable=invalid-name, unused-argument 42 | 43 | if rc == 0: 44 | if len(userdata) > 0: 45 | _do_publish(client) 46 | else: 47 | raise mqtt.MQTTException(paho.connack_string(rc)) 48 | 49 | 50 | def _on_publish(client, userdata, mid): 51 | """Internal callback""" 52 | #pylint: disable=unused-argument 53 | 54 | if len(userdata) == 0: 55 | client.disconnect() 56 | else: 57 | _do_publish(client) 58 | 59 | 60 | def multiple(msgs, hostname="localhost", port=1883, client_id="", keepalive=60, 61 | will=None, auth=None, tls=None, protocol=paho.MQTTv311, 62 | transport="tcp"): 63 | """Publish multiple messages to a broker, then disconnect cleanly. 64 | 65 | This function creates an MQTT client, connects to a broker and publishes a 66 | list of messages. Once the messages have been delivered, it disconnects 67 | cleanly from the broker. 68 | 69 | msgs : a list of messages to publish. Each message is either a dict or a 70 | tuple. 71 | 72 | If a dict, only the topic must be present. Default values will be 73 | used for any missing arguments. The dict must be of the form: 74 | 75 | msg = {'topic':"", 'payload':"", 'qos':, 76 | 'retain':} 77 | topic must be present and may not be empty. 78 | If payload is "", None or not present then a zero length payload 79 | will be published. 80 | If qos is not present, the default of 0 is used. 81 | If retain is not present, the default of False is used. 82 | 83 | If a tuple, then it must be of the form: 84 | ("", "", qos, retain) 85 | 86 | hostname : a string containing the address of the broker to connect to. 87 | Defaults to localhost. 88 | 89 | port : the port to connect to the broker on. Defaults to 1883. 90 | 91 | client_id : the MQTT client id to use. If "" or None, the Paho library will 92 | generate a client id automatically. 93 | 94 | keepalive : the keepalive timeout value for the client. Defaults to 60 95 | seconds. 96 | 97 | will : a dict containing will parameters for the client: will = {'topic': 98 | "", 'payload':", 'qos':, 'retain':}. 99 | Topic is required, all other parameters are optional and will 100 | default to None, 0 and False respectively. 101 | Defaults to None, which indicates no will should be used. 102 | 103 | auth : a dict containing authentication parameters for the client: 104 | auth = {'username':"", 'password':""} 105 | Username is required, password is optional and will default to None 106 | if not provided. 107 | Defaults to None, which indicates no authentication is to be used. 108 | 109 | tls : a dict containing TLS configuration parameters for the client: 110 | dict = {'ca_certs':"", 'certfile':"", 111 | 'keyfile':"", 'tls_version':"", 112 | 'ciphers':"} 113 | ca_certs is required, all other parameters are optional and will 114 | default to None if not provided, which results in the client using 115 | the default behaviour - see the paho.mqtt.client documentation. 116 | Alternatively, tls input can be an SSLContext object, which will be 117 | processed using the tls_set_context method. 118 | Defaults to None, which indicates that TLS should not be used. 119 | 120 | transport : set to "tcp" to use the default setting of transport which is 121 | raw TCP. Set to "websockets" to use WebSockets as the transport. 122 | """ 123 | 124 | if not isinstance(msgs, list): 125 | raise ValueError('msgs must be a list') 126 | 127 | client = paho.Client(client_id=client_id, 128 | userdata=msgs, protocol=protocol, transport=transport) 129 | 130 | client.on_publish = _on_publish 131 | client.on_connect = _on_connect 132 | 133 | if auth: 134 | username = auth.get('username') 135 | if username: 136 | password = auth.get('password') 137 | client.username_pw_set(username, password) 138 | else: 139 | raise KeyError("The 'username' key was not found, this is " 140 | "required for auth") 141 | 142 | if will is not None: 143 | client.will_set(**will) 144 | 145 | if tls is not None: 146 | if isinstance(tls, dict): 147 | client.tls_set(**tls) 148 | else: 149 | # Assume input is SSLContext object 150 | client.tls_set_context(tls) 151 | 152 | client.connect(hostname, port, keepalive) 153 | client.loop_forever() 154 | 155 | 156 | def single(topic, payload=None, qos=0, retain=False, hostname="localhost", 157 | port=1883, client_id="", keepalive=60, will=None, auth=None, 158 | tls=None, protocol=paho.MQTTv311, transport="tcp"): 159 | """Publish a single message to a broker, then disconnect cleanly. 160 | 161 | This function creates an MQTT client, connects to a broker and publishes a 162 | single message. Once the message has been delivered, it disconnects cleanly 163 | from the broker. 164 | 165 | topic : the only required argument must be the topic string to which the 166 | payload will be published. 167 | 168 | payload : the payload to be published. If "" or None, a zero length payload 169 | will be published. 170 | 171 | qos : the qos to use when publishing, default to 0. 172 | 173 | retain : set the message to be retained (True) or not (False). 174 | 175 | hostname : a string containing the address of the broker to connect to. 176 | Defaults to localhost. 177 | 178 | port : the port to connect to the broker on. Defaults to 1883. 179 | 180 | client_id : the MQTT client id to use. If "" or None, the Paho library will 181 | generate a client id automatically. 182 | 183 | keepalive : the keepalive timeout value for the client. Defaults to 60 184 | seconds. 185 | 186 | will : a dict containing will parameters for the client: will = {'topic': 187 | "", 'payload':", 'qos':, 'retain':}. 188 | Topic is required, all other parameters are optional and will 189 | default to None, 0 and False respectively. 190 | Defaults to None, which indicates no will should be used. 191 | 192 | auth : a dict containing authentication parameters for the client: 193 | auth = {'username':"", 'password':""} 194 | Username is required, password is optional and will default to None 195 | if not provided. 196 | Defaults to None, which indicates no authentication is to be used. 197 | 198 | tls : a dict containing TLS configuration parameters for the client: 199 | dict = {'ca_certs':"", 'certfile':"", 200 | 'keyfile':"", 'tls_version':"", 201 | 'ciphers':"} 202 | ca_certs is required, all other parameters are optional and will 203 | default to None if not provided, which results in the client using 204 | the default behaviour - see the paho.mqtt.client documentation. 205 | Defaults to None, which indicates that TLS should not be used. 206 | Alternatively, tls input can be an SSLContext object, which will be 207 | processed using the tls_set_context method. 208 | 209 | transport : set to "tcp" to use the default setting of transport which is 210 | raw TCP. Set to "websockets" to use WebSockets as the transport. 211 | """ 212 | 213 | msg = {'topic':topic, 'payload':payload, 'qos':qos, 'retain':retain} 214 | 215 | multiple([msg], hostname, port, client_id, keepalive, will, auth, tls, 216 | protocol, transport) 217 | -------------------------------------------------------------------------------- /paho/mqtt/subscribe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Roger Light 2 | # 3 | # All rights reserved. This program and the accompanying materials 4 | # are made available under the terms of the Eclipse Public License v1.0 5 | # and Eclipse Distribution License v1.0 which accompany this distribution. 6 | # 7 | # The Eclipse Public License is available at 8 | # http://www.eclipse.org/legal/epl-v10.html 9 | # and the Eclipse Distribution License is available at 10 | # http://www.eclipse.org/org/documents/edl-v10.php. 11 | # 12 | # Contributors: 13 | # Roger Light - initial API and implementation 14 | 15 | """ 16 | This module provides some helper functions to allow straightforward subscribing 17 | to topics and retrieving messages. The two functions are simple(), which 18 | returns one or messages matching a set of topics, and callback() which allows 19 | you to pass a callback for processing of messages. 20 | """ 21 | 22 | import paho.mqtt.client as paho 23 | import paho.mqtt as mqtt 24 | 25 | 26 | def _on_connect(client, userdata, flags, rc): 27 | """Internal callback""" 28 | if rc != 0: 29 | raise mqtt.MQTTException(paho.connack_string(rc)) 30 | 31 | if isinstance(userdata['topics'], list): 32 | for topic in userdata['topics']: 33 | client.subscribe(topic, userdata['qos']) 34 | else: 35 | client.subscribe(userdata['topics'], userdata['qos']) 36 | 37 | 38 | def _on_message_callback(client, userdata, message): 39 | """Internal callback""" 40 | userdata['callback'](client, userdata['userdata'], message) 41 | 42 | 43 | def _on_message_simple(client, userdata, message): 44 | """Internal callback""" 45 | 46 | if userdata['msg_count'] == 0: 47 | return 48 | 49 | # Don't process stale retained messages if 'retained' was false 50 | if message.retain and not userdata['retained']: 51 | return 52 | 53 | userdata['msg_count'] = userdata['msg_count'] - 1 54 | 55 | if userdata['messages'] is None and userdata['msg_count'] == 0: 56 | userdata['messages'] = message 57 | client.disconnect() 58 | return 59 | 60 | userdata['messages'].append(message) 61 | if userdata['msg_count'] == 0: 62 | client.disconnect() 63 | 64 | 65 | def callback(callback, topics, qos=0, userdata=None, hostname="localhost", 66 | port=1883, client_id="", keepalive=60, will=None, auth=None, 67 | tls=None, protocol=paho.MQTTv311, transport="tcp", 68 | clean_session=True): 69 | """Subscribe to a list of topics and process them in a callback function. 70 | 71 | This function creates an MQTT client, connects to a broker and subscribes 72 | to a list of topics. Incoming messages are processed by the user provided 73 | callback. This is a blocking function and will never return. 74 | 75 | callback : function of the form "on_message(client, userdata, message)" for 76 | processing the messages received. 77 | 78 | topics : either a string containing a single topic to subscribe to, or a 79 | list of topics to subscribe to. 80 | 81 | qos : the qos to use when subscribing. This is applied to all topics. 82 | 83 | userdata : passed to the callback 84 | 85 | hostname : a string containing the address of the broker to connect to. 86 | Defaults to localhost. 87 | 88 | port : the port to connect to the broker on. Defaults to 1883. 89 | 90 | client_id : the MQTT client id to use. If "" or None, the Paho library will 91 | generate a client id automatically. 92 | 93 | keepalive : the keepalive timeout value for the client. Defaults to 60 94 | seconds. 95 | 96 | will : a dict containing will parameters for the client: will = {'topic': 97 | "", 'payload':", 'qos':, 'retain':}. 98 | Topic is required, all other parameters are optional and will 99 | default to None, 0 and False respectively. 100 | Defaults to None, which indicates no will should be used. 101 | 102 | auth : a dict containing authentication parameters for the client: 103 | auth = {'username':"", 'password':""} 104 | Username is required, password is optional and will default to None 105 | if not provided. 106 | Defaults to None, which indicates no authentication is to be used. 107 | 108 | tls : a dict containing TLS configuration parameters for the client: 109 | dict = {'ca_certs':"", 'certfile':"", 110 | 'keyfile':"", 'tls_version':"", 111 | 'ciphers':"} 112 | ca_certs is required, all other parameters are optional and will 113 | default to None if not provided, which results in the client using 114 | the default behaviour - see the paho.mqtt.client documentation. 115 | Alternatively, tls input can be an SSLContext object, which will be 116 | processed using the tls_set_context method. 117 | Defaults to None, which indicates that TLS should not be used. 118 | 119 | transport : set to "tcp" to use the default setting of transport which is 120 | raw TCP. Set to "websockets" to use WebSockets as the transport. 121 | 122 | clean_session : a boolean that determines the client type. If True, 123 | the broker will remove all information about this client 124 | when it disconnects. If False, the client is a persistent 125 | client and subscription information and queued messages 126 | will be retained when the client disconnects. 127 | Defaults to True. 128 | """ 129 | 130 | if qos < 0 or qos > 2: 131 | raise ValueError('qos must be in the range 0-2') 132 | 133 | callback_userdata = { 134 | 'callback':callback, 135 | 'topics':topics, 136 | 'qos':qos, 137 | 'userdata':userdata} 138 | 139 | client = paho.Client(client_id=client_id, userdata=callback_userdata, 140 | protocol=protocol, transport=transport, 141 | clean_session=clean_session) 142 | client.on_message = _on_message_callback 143 | client.on_connect = _on_connect 144 | 145 | if auth: 146 | username = auth.get('username') 147 | if username: 148 | password = auth.get('password') 149 | client.username_pw_set(username, password) 150 | else: 151 | raise KeyError("The 'username' key was not found, this is " 152 | "required for auth") 153 | 154 | if will is not None: 155 | client.will_set(**will) 156 | 157 | if tls is not None: 158 | if isinstance(tls, dict): 159 | client.tls_set(**tls) 160 | else: 161 | # Assume input is SSLContext object 162 | client.tls_set_context(tls) 163 | 164 | client.connect(hostname, port, keepalive) 165 | client.loop_forever() 166 | 167 | 168 | def simple(topics, qos=0, msg_count=1, retained=True, hostname="localhost", 169 | port=1883, client_id="", keepalive=60, will=None, auth=None, 170 | tls=None, protocol=paho.MQTTv311, transport="tcp", 171 | clean_session=True): 172 | """Subscribe to a list of topics and return msg_count messages. 173 | 174 | This function creates an MQTT client, connects to a broker and subscribes 175 | to a list of topics. Once "msg_count" messages have been received, it 176 | disconnects cleanly from the broker and returns the messages. 177 | 178 | topics : either a string containing a single topic to subscribe to, or a 179 | list of topics to subscribe to. 180 | 181 | qos : the qos to use when subscribing. This is applied to all topics. 182 | 183 | msg_count : the number of messages to retrieve from the broker. 184 | if msg_count == 1 then a single MQTTMessage will be returned. 185 | if msg_count > 1 then a list of MQTTMessages will be returned. 186 | 187 | retained : If set to True, retained messages will be processed the same as 188 | non-retained messages. If set to False, retained messages will 189 | be ignored. This means that with retained=False and msg_count=1, 190 | the function will return the first message received that does 191 | not have the retained flag set. 192 | 193 | hostname : a string containing the address of the broker to connect to. 194 | Defaults to localhost. 195 | 196 | port : the port to connect to the broker on. Defaults to 1883. 197 | 198 | client_id : the MQTT client id to use. If "" or None, the Paho library will 199 | generate a client id automatically. 200 | 201 | keepalive : the keepalive timeout value for the client. Defaults to 60 202 | seconds. 203 | 204 | will : a dict containing will parameters for the client: will = {'topic': 205 | "", 'payload':", 'qos':, 'retain':}. 206 | Topic is required, all other parameters are optional and will 207 | default to None, 0 and False respectively. 208 | Defaults to None, which indicates no will should be used. 209 | 210 | auth : a dict containing authentication parameters for the client: 211 | auth = {'username':"", 'password':""} 212 | Username is required, password is optional and will default to None 213 | if not provided. 214 | Defaults to None, which indicates no authentication is to be used. 215 | 216 | tls : a dict containing TLS configuration parameters for the client: 217 | dict = {'ca_certs':"", 'certfile':"", 218 | 'keyfile':"", 'tls_version':"", 219 | 'ciphers':"} 220 | ca_certs is required, all other parameters are optional and will 221 | default to None if not provided, which results in the client using 222 | the default behaviour - see the paho.mqtt.client documentation. 223 | Alternatively, tls input can be an SSLContext object, which will be 224 | processed using the tls_set_context method. 225 | Defaults to None, which indicates that TLS should not be used. 226 | 227 | transport : set to "tcp" to use the default setting of transport which is 228 | raw TCP. Set to "websockets" to use WebSockets as the transport. 229 | 230 | clean_session : a boolean that determines the client type. If True, 231 | the broker will remove all information about this client 232 | when it disconnects. If False, the client is a persistent 233 | client and subscription information and queued messages 234 | will be retained when the client disconnects. 235 | Defaults to True. 236 | """ 237 | 238 | if msg_count < 1: 239 | raise ValueError('msg_count must be > 0') 240 | 241 | # Set ourselves up to return a single message if msg_count == 1, or a list 242 | # if > 1. 243 | if msg_count == 1: 244 | messages = None 245 | else: 246 | messages = [] 247 | 248 | userdata = {'retained':retained, 'msg_count':msg_count, 'messages':messages} 249 | 250 | callback(_on_message_simple, topics, qos, userdata, hostname, port, 251 | client_id, keepalive, will, auth, tls, protocol, transport, 252 | clean_session) 253 | 254 | return userdata['messages'] 255 | -------------------------------------------------------------------------------- /py-duco-mqtt.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Python DUCO MQTT bridge 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | User=root 8 | WorkingDirectory=/usr/lib/py-duco-mqtt 9 | ExecStart=/usr/bin/python . 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martenjacobs/py-duco-mqtt/6640015a8519a6936951c3f6b4f8cbd2c18ba820/test.py -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | 2 | def pathify(src, root="", separator="/"): 3 | for (k, v) in src.iteritems(): 4 | topic = separator.join((root, k)) 5 | if type(v)==dict: 6 | for i in pathify(v, topic, separator): 7 | yield i 8 | else: 9 | yield (topic, v) 10 | 11 | 12 | def changes(old, new): 13 | old_k=set(old.keys()) 14 | new_k=set(new.keys()) 15 | 16 | _ret = {} 17 | for k in (new_k-old_k): 18 | _ret[k] = new[k] 19 | for k in (old_k-new_k): 20 | _ret[k] = None 21 | for k in old_k.intersection(new_k): 22 | if type(old[k])!=type(new[k]): 23 | _ret[k] = new[k] 24 | if type(new[k])==dict: 25 | inner = changes(old[k], new[k]) 26 | if inner: 27 | _ret[k] = inner 28 | elif old[k] != new[k]: 29 | _ret[k] = new[k] 30 | return _ret 31 | 32 | 33 | def merge(iterator): 34 | tgt = {} 35 | for i in iterator: 36 | tgt.update(i) 37 | return tgt 38 | -------------------------------------------------------------------------------- /wire-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martenjacobs/py-duco-mqtt/6640015a8519a6936951c3f6b4f8cbd2c18ba820/wire-diagram.png --------------------------------------------------------------------------------