/ adafruit_aws_iot.py
adafruit_aws_iot.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2019 Brent Rubell for Adafruit Industries 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 13 # all 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 21 # THE SOFTWARE. 22 """ 23 `adafruit_aws_iot` 24 ================================================================================ 25 26 Amazon AWS IoT MQTT Client for CircuitPython 27 28 29 * Author(s): Brent Rubell 30 31 Implementation Notes 32 -------------------- 33 34 **Hardware:** 35 36 37 **Software and Dependencies:** 38 39 * Adafruit CircuitPython firmware for the supported boards: 40 https://github.com/adafruit/circuitpython/releases 41 42 """ 43 import json 44 from adafruit_minimqtt import MMQTTException 45 46 __version__ = "0.0.0-auto.0" 47 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_AWS_IOT.git" 48 49 50 class AWS_IOT_ERROR(Exception): 51 """Exception raised on MQTT API return-code errors.""" 52 53 # pylint: disable=unnecessary-pass 54 pass 55 56 57 class MQTT_CLIENT: 58 """Client for interacting with Amazon AWS IoT MQTT API. 59 60 :param MiniMQTT mmqttclient: Pre-configured MiniMQTT Client object. 61 :param int keep_alive: Optional Keep-alive timer interval, in seconds. 62 Provided interval must be 30 <= keep_alive <= 1200. 63 64 """ 65 66 def __init__(self, mmqttclient, keep_alive=30): 67 if "MQTT" in str(type(mmqttclient)): 68 self.client = mmqttclient 69 else: 70 raise TypeError( 71 "This class requires a preconfigured MiniMQTT object, \ 72 please create one." 73 ) 74 # Verify MiniMQTT client object configuration 75 try: 76 self.cid = self.client.client_id 77 assert ( 78 self.cid[0] != "$" 79 ), "Client ID can not start with restricted client ID prefix $." 80 except: 81 raise TypeError( 82 "You must provide MiniMQTT with your AWS IoT Device's Identifier \ 83 as the Client ID." 84 ) 85 # Shadow-interaction topic 86 self.shadow_topic = "$aws/things/{}/shadow".format(self.cid) 87 # keep_alive timer must be between 30 <= keep alive interval <= 1200 seconds 88 # https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html 89 assert ( 90 30 <= keep_alive <= 1200 91 ), "Keep_Alive timer \ 92 interval must be between 30 and 1200 seconds" 93 self.keep_alive = keep_alive 94 # User-defined MQTT callback methods must be init'd to None 95 self.on_connect = None 96 self.on_disconnect = None 97 self.on_message = None 98 self.on_subscribe = None 99 self.on_unsubscribe = None 100 # Connect MiniMQTT callback handlers 101 self.client.on_connect = self._on_connect_mqtt 102 self.client.on_disconnect = self._on_disconnect_mqtt 103 self.client.on_message = self._on_message_mqtt 104 self.client.on_subscribe = self._on_subscribe_mqtt 105 self.client.on_unsubscribe = self._on_unsubscribe_mqtt 106 self.connected_to_aws = False 107 108 def __enter__(self): 109 return self 110 111 def __exit__(self, exception_type, exception_value, traceback): 112 self.disconnect() 113 114 @property 115 def is_connected(self): 116 """Returns if MQTT_CLIENT is connected to AWS IoT MQTT Broker 117 118 """ 119 return self.connected_to_aws 120 121 def disconnect(self): 122 """Disconnects from Amazon AWS IoT MQTT Broker and de-initializes the MiniMQTT Client. 123 124 """ 125 try: 126 self.client.disconnect() 127 except MMQTTException as error: 128 raise AWS_IOT_ERROR("Error disconnecting with AWS IoT: ", error) 129 self.connected_to_aws = False 130 # Reset user-defined callback methods to None 131 self.on_connect = None 132 self.on_disconnect = None 133 self.on_message = None 134 self.on_subscribe = None 135 self.on_unsubscribe = None 136 self.client.deinit() 137 138 def reconnect(self): 139 """Reconnects to the AWS IoT MQTT Broker 140 141 """ 142 try: 143 self.client.reconnect() 144 except MMQTTException as error: 145 raise AWS_IOT_ERROR("Error re-connecting to AWS IoT:", error) 146 147 def connect(self, clean_session=True): 148 """Connects to Amazon AWS IoT MQTT Broker with Client ID. 149 :param bool clean_session: Establishes a clean session with AWS broker. 150 151 """ 152 try: 153 self.client.connect(clean_session) 154 except MMQTTException as error: 155 raise AWS_IOT_ERROR("Error connecting to AWS IoT: ", error) 156 self.connected_to_aws = True 157 158 # MiniMQTT Callback Handlers 159 # pylint: disable=not-callable, unused-argument 160 def _on_connect_mqtt(self, client, userdata, flag, ret_code): 161 """Runs when code calls on_connect. 162 :param MiniMQTT client: MiniMQTT client object. 163 :param str user_data: User data from broker 164 :param int flag: QoS flag from broker. 165 :param int ret_code: Return code from broker. 166 167 """ 168 self.connected_to_aws = True 169 # Call the on_connect callback if defined in code 170 if self.on_connect is not None: 171 self.on_connect(self, userdata, flag, ret_code) 172 173 # pylint: disable=not-callable, unused-argument 174 def _on_disconnect_mqtt(self, client, userdata, flag, ret_code): 175 """Runs when code calls on_disconnect. 176 :param MiniMQTT client: MiniMQTT client object. 177 :param str user_data: User data from broker 178 :param int flag: QoS flag from broker. 179 :param int ret_code: Return code from broker. 180 181 """ 182 self.connected_to_aws = False 183 # Call the on_connect callback if defined in code 184 if self.on_connect is not None: 185 self.on_connect(self, userdata, flag, ret_code) 186 187 # pylint: disable=not-callable 188 def _on_message_mqtt(self, client, topic, payload): 189 """Runs when the client calls on_message. 190 :param MiniMQTT client: MiniMQTT client object. 191 :param str topic: MQTT broker topic. 192 :param str payload: Payload returned by MQTT broker topic 193 194 """ 195 if self.on_message is not None: 196 self.on_message(self, topic, payload) 197 198 # pylint: disable=not-callable 199 def _on_subscribe_mqtt(self, client, user_data, topic, qos): 200 """Runs when the client calls on_subscribe. 201 202 :param MiniMQTT client: MiniMQTT client object. 203 :param str user_data: User data from broker 204 :param str topic: Desired MQTT topic. 205 param int qos: Quality of service level for topic, from broker. 206 207 """ 208 if self.on_subscribe is not None: 209 self.on_subscribe(self, user_data, topic, qos) 210 211 # pylint: disable=not-callable 212 def _on_unsubscribe_mqtt(self, client, user_data, topic, pid): 213 """Runs when the client calls on_unsubscribe. 214 """ 215 if self.on_unsubscribe is not None: 216 self.on_unsubscribe(self, user_data, topic, pid) 217 218 # MiniMQTT Network Control Flow 219 def loop(self): 220 """ Starts a synchronous message loop which maintains connection with AWS IoT. 221 Must be called within the keep_alive timeout specified to init. 222 This method does not handle network connection/disconnection. 223 224 Example of "pumping" an AWS IoT message loop: 225 ..code-block::python 226 227 while True: 228 aws_iot.loop() 229 230 """ 231 if self.connected_to_aws: 232 self.client.loop() 233 234 def loop_forever(self): 235 """Begins a blocking, asynchronous message loop. 236 This method handles network connection/disconnection. 237 238 """ 239 if self.connected_to_aws: 240 self.client.loop_forever() 241 242 @staticmethod 243 def validate_topic(topic): 244 """Validates if user-provided pub/sub topics adhere to AWS Service Limits. 245 https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html 246 :param str topic: Desired topic to validate 247 248 """ 249 assert hasattr(topic, "split"), "Topic must be a string" 250 assert len(topic) < 256, "Topic must be less than 256 bytes!" 251 assert len(topic.split("/")) <= 9, "Topics are limited to 7 forward slashes." 252 253 # MiniMQTT Pub/Sub Methods, for usage with AWS IoT 254 def subscribe(self, topic, qos=1): 255 """Subscribes to an AWS IoT Topic. 256 :param str topic: MQTT topic to subscribe to. 257 :param int qos: Desired topic subscription's quality-of-service. 258 259 """ 260 assert qos < 2, "AWS IoT does not support subscribing with QoS 2." 261 self.validate_topic(topic) 262 self.client.subscribe(topic, qos) 263 264 def publish(self, topic, payload, qos=1): 265 """Publishes to a AWS IoT Topic. 266 :param str topic: MQTT topic to publish to. 267 :param str payload: Data to publish to topic. 268 :param int payload: Data to publish to topic. 269 :param float payload: Data to publish to topic. 270 :param json payload: JSON-formatted data to publish to topic. 271 :param int qos: Quality of service level for publishing. 272 273 """ 274 assert qos < 2, "AWS IoT does not support publishing with QoS 2." 275 self.validate_topic(topic) 276 if isinstance(payload, int or float): 277 payload = str(payload) 278 self.client.publish(topic, payload, qos=qos) 279 280 # AWS IoT Device Shadow Service 281 282 def shadow_get_subscribe(self, qos=1): 283 """Subscribes to device's shadow get response. 284 :param int qos: Optional quality of service level. 285 286 """ 287 self.client.subscribe(self.shadow_topic + "/get/#", qos) 288 289 def shadow_subscribe(self, qos=1): 290 """Subscribes to all notifications on the device's shadow update topic. 291 :param int qos: Optional quality of service level. 292 293 """ 294 self.client.subscribe(self.shadow_topic + "/update/#", qos) 295 296 def shadow_update(self, document): 297 """Publishes a request state document to update the device's shadow. 298 :param json state_document: JSON-formatted state document. 299 300 """ 301 self.client.publish(self.shadow_topic + "/update", document) 302 303 def shadow_get(self): 304 """Publishes an empty message to shadow get topic to get the device's shadow. 305 306 """ 307 self.client.publish( 308 self.shadow_topic + "/get", json.dumps({"message": "ignore"}) 309 ) 310 311 def shadow_delete(self): 312 """Publishes an empty message to the shadow delete topic to delete a device's shadow 313 314 """ 315 self.client.publish( 316 self.shadow_topic + "/delete", json.dumps({"message": "delete"}) 317 )