/ 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)