/ adafruit_azureiot / iotcentral_device.py
iotcentral_device.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2020 Jim Bennett 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 `iotcentral_device` 24 ===================== 25 26 Connectivity to Azure IoT Central 27 28 * Author(s): Jim Bennett, Elena Horton 29 """ 30 31 import json 32 import time 33 import adafruit_logging as logging 34 from .device_registration import DeviceRegistration 35 from .iot_error import IoTError 36 from .iot_mqtt import IoTMQTT, IoTMQTTCallback, IoTResponse 37 38 39 class IoTCentralDevice(IoTMQTTCallback): 40 """A device client for the Azure IoT Central service 41 """ 42 43 def connection_status_change(self, connected: bool) -> None: 44 """Called when the connection status changes 45 :param bool connected: True if the device is connected, otherwise false 46 """ 47 if self.on_connection_status_changed is not None: 48 # pylint: disable=E1102 49 self.on_connection_status_changed(connected) 50 51 # pylint: disable=W0613, R0201 52 def direct_method_called(self, method_name: str, payload: str) -> IoTResponse: 53 """Called when a direct method is invoked 54 :param str method_name: The name of the method that was invoked 55 :param str payload: The payload with the message 56 :returns: A response with a code and status to show if the method was correctly handled 57 :rtype: IoTResponse 58 """ 59 if self.on_command_executed is not None: 60 # pylint: disable=E1102 61 return self.on_command_executed(method_name, payload) 62 63 raise IoTError("on_command_executed not set") 64 65 def device_twin_desired_updated(self, desired_property_name: str, desired_property_value, desired_version: int) -> None: 66 """Called when the device twin desired properties are updated 67 :param str desired_property_name: The name of the desired property that was updated 68 :param desired_property_value: The value of the desired property that was updated 69 :param int desired_version: The version of the desired property that was updated 70 """ 71 if self.on_property_changed is not None: 72 # pylint: disable=E1102 73 self.on_property_changed(desired_property_name, desired_property_value, desired_version) 74 75 # when a desired property changes, update the reported to match to keep them in sync 76 self.send_property(desired_property_name, desired_property_value) 77 78 def device_twin_reported_updated(self, reported_property_name: str, reported_property_value, reported_version: int) -> None: 79 """Called when the device twin reported values are updated 80 :param str reported_property_name: The name of the reported property that was updated 81 :param reported_property_value: The value of the reported property that was updated 82 :param int reported_version: The version of the reported property that was updated 83 """ 84 if self.on_property_changed is not None: 85 # pylint: disable=E1102 86 self.on_property_changed(reported_property_name, reported_property_value, reported_version) 87 88 # pylint: disable=R0913 89 def __init__(self, socket, iface, id_scope: str, device_id: str, key: str, token_expires: int = 21600, logger: logging = None): 90 """Create the Azure IoT Central device client 91 :param socket: The network socket 92 :param iface: The network interface 93 :param str id_scope: The ID Scope of the device in IoT Central 94 :param str device_id: The device ID of the device in IoT Central 95 :param str key: The primary or secondary key of the device in IoT Central 96 :param int token_expires: The number of seconds till the token expires, defaults to 6 hours 97 :param adafruit_logging logger: The logger 98 """ 99 self._socket = socket 100 self._iface = iface 101 self._id_scope = id_scope 102 self._device_id = device_id 103 self._key = key 104 self._token_expires = token_expires 105 self._logger = logger if logger is not None else logging.getLogger("log") 106 self._device_registration = None 107 self._mqtt = None 108 109 self.on_connection_status_changed = None 110 """A callback method that is called when the connection status is changed. This method should have the following signature: 111 def connection_status_changed(connected: bool) -> None 112 """ 113 114 self.on_command_executed = None 115 """A callback method that is called when a command is executed on the device. This method should have the following signature: 116 def connection_status_changed(method_name: str, payload: str) -> IoTResponse: 117 118 This method returns an IoTResponse containing a status code and message from the command call. Set this appropriately 119 depending on if the command was successfully handled or not. For example, if the command was handled successfully, set 120 the code to 200 and message to "OK": 121 122 return IoTResponse(200, "OK") 123 """ 124 125 self.on_property_changed = None 126 """A callback method that is called when property values are updated. This method should have the following signature: 127 def property_changed(_property_name: str, property_value, version: int) -> None 128 """ 129 130 def connect(self) -> None: 131 """Connects to Azure IoT Central 132 :raises DeviceRegistrationError: if the device cannot be registered successfully 133 :raises RuntimeError: if the internet connection is not responding or is unable to connect 134 """ 135 self._device_registration = DeviceRegistration(self._socket, self._id_scope, self._device_id, self._key, self._logger) 136 137 token_expiry = int(time.time() + self._token_expires) 138 hostname = self._device_registration.register_device(token_expiry) 139 self._mqtt = IoTMQTT(self, self._socket, self._iface, hostname, self._device_id, self._key, self._token_expires, self._logger) 140 141 self._mqtt.connect() 142 self._mqtt.subscribe_to_twins() 143 144 def disconnect(self) -> None: 145 """Disconnects from the MQTT broker 146 :raises IoTError: if there is no open connection to the MQTT broker 147 """ 148 if self._mqtt is None: 149 raise IoTError("You are not connected to IoT Central") 150 151 self._mqtt.disconnect() 152 153 def reconnect(self) -> None: 154 """Reconnects to the MQTT broker 155 """ 156 if self._mqtt is None: 157 raise IoTError("You are not connected to IoT Central") 158 159 self._mqtt.reconnect() 160 161 def is_connected(self) -> bool: 162 """Gets if there is an open connection to the MQTT broker 163 :returns: True if there is an open connection, False if not 164 :rtype: bool 165 """ 166 if self._mqtt is not None: 167 return self._mqtt.is_connected() 168 169 return False 170 171 def loop(self) -> None: 172 """Listens for MQTT messages 173 :raises IoTError: if there is no open connection to the MQTT broker 174 """ 175 if self._mqtt is None: 176 raise IoTError("You are not connected to IoT Central") 177 178 self._mqtt.loop() 179 180 def send_property(self, property_name: str, value) -> None: 181 """Updates the value of a writable property 182 :param str property_name: The name of the property to write to 183 :param value: The value to set on the property 184 :raises IoTError: if there is no open connection to the MQTT broker 185 """ 186 if self._mqtt is None: 187 raise IoTError("You are not connected to IoT Central") 188 189 patch_json = {property_name: value} 190 patch = json.dumps(patch_json) 191 self._mqtt.send_twin_patch(patch) 192 193 def send_telemetry(self, data) -> None: 194 """Sends telemetry to the IoT Central app 195 :param data: The telemetry data to send 196 :raises IoTError: if there is no open connection to the MQTT broker 197 """ 198 if self._mqtt is None: 199 raise IoTError("You are not connected to IoT Central") 200 201 if isinstance(data, dict): 202 data = json.dumps(data) 203 204 self._mqtt.send_device_to_cloud_message(data)