/ adafruit_sht31d.py
adafruit_sht31d.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2017 Jerry Needell
  4  # Copyright (c) 2019 Llewelyn Trahaearn
  5  #
  6  # Permission is hereby granted, free of charge, to any person obtaining a copy
  7  # of this software and associated documentation files (the "Software"), to deal
  8  # in the Software without restriction, including without limitation the rights
  9  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10  # copies of the Software, and to permit persons to whom the Software is
 11  # furnished to do so, subject to the following conditions:
 12  #
 13  # The above copyright notice and this permission notice shall be included in
 14  # all copies or substantial portions of the Software.
 15  #
 16  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 22  # THE SOFTWARE.
 23  """
 24  `adafruit_sht31d`
 25  ====================================================
 26  
 27  This is a CircuitPython driver for the SHT31-D temperature and humidity sensor.
 28  
 29  * Author(s): Jerry Needell, Llewelyn Trahaearn
 30  
 31  Implementation Notes
 32  --------------------
 33  
 34  **Hardware:**
 35  
 36  * Adafruit `Sensiron SHT31-D Temperature & Humidity Sensor Breakout
 37    <https://www.adafruit.com/product/2857>`_ (Product ID: 2857)
 38  
 39  **Software and Dependencies:**
 40  
 41  * Adafruit CircuitPython firmware for the ESP8622 and M0-based boards:
 42    https://github.com/adafruit/circuitpython/releases
 43  * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
 44  """
 45  
 46  # imports
 47  try:
 48      import struct
 49  except ImportError:
 50      import ustruct as struct
 51  
 52  import time
 53  
 54  from micropython import const
 55  from adafruit_bus_device.i2c_device import I2CDevice
 56  
 57  __version__ = "0.0.0-auto.0"
 58  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_SHT31D.git"
 59  
 60  
 61  _SHT31_DEFAULT_ADDRESS = const(0x44)
 62  _SHT31_SECONDARY_ADDRESS = const(0x45)
 63  
 64  _SHT31_ADDRESSES = (_SHT31_DEFAULT_ADDRESS, _SHT31_SECONDARY_ADDRESS)
 65  
 66  _SHT31_READSERIALNBR = const(0x3780)
 67  _SHT31_READSTATUS = const(0xF32D)
 68  _SHT31_CLEARSTATUS = const(0x3041)
 69  _SHT31_HEATER_ENABLE = const(0x306D)
 70  _SHT31_HEATER_DISABLE = const(0x3066)
 71  _SHT31_SOFTRESET = const(0x30A2)
 72  _SHT31_NOSLEEP = const(0x303E)
 73  _SHT31_PERIODIC_FETCH = const(0xE000)
 74  _SHT31_PERIODIC_BREAK = const(0x3093)
 75  
 76  MODE_SINGLE = "Single"
 77  MODE_PERIODIC = "Periodic"
 78  
 79  _SHT31_MODES = (MODE_SINGLE, MODE_PERIODIC)
 80  
 81  REP_HIGH = "High"
 82  REP_MED = "Medium"
 83  REP_LOW = "Low"
 84  
 85  _SHT31_REP = (REP_HIGH, REP_MED, REP_LOW)
 86  
 87  FREQUENCY_0_5 = 0.5
 88  FREQUENCY_1 = 1
 89  FREQUENCY_2 = 2
 90  FREQUENCY_4 = 4
 91  FREQUENCY_10 = 10
 92  
 93  _SHT31_FREQUENCIES = (
 94      FREQUENCY_0_5,
 95      FREQUENCY_1,
 96      FREQUENCY_2,
 97      FREQUENCY_4,
 98      FREQUENCY_10,
 99  )
100  
101  _SINGLE_COMMANDS = (
102      (REP_LOW, const(False), const(0x2416)),
103      (REP_MED, const(False), const(0x240B)),
104      (REP_HIGH, const(False), const(0x2400)),
105      (REP_LOW, const(True), const(0x2C10)),
106      (REP_MED, const(True), const(0x2C0D)),
107      (REP_HIGH, const(True), const(0x2C06)),
108  )
109  
110  _PERIODIC_COMMANDS = (
111      (True, None, const(0x2B32)),
112      (REP_LOW, FREQUENCY_0_5, const(0x202F)),
113      (REP_MED, FREQUENCY_0_5, const(0x2024)),
114      (REP_HIGH, FREQUENCY_0_5, const(0x2032)),
115      (REP_LOW, FREQUENCY_1, const(0x212D)),
116      (REP_MED, FREQUENCY_1, const(0x2126)),
117      (REP_HIGH, FREQUENCY_1, const(0x2130)),
118      (REP_LOW, FREQUENCY_2, const(0x222B)),
119      (REP_MED, FREQUENCY_2, const(0x2220)),
120      (REP_HIGH, FREQUENCY_2, const(0x2236)),
121      (REP_LOW, FREQUENCY_4, const(0x2329)),
122      (REP_MED, FREQUENCY_4, const(0x2322)),
123      (REP_HIGH, FREQUENCY_4, const(0x2334)),
124      (REP_LOW, FREQUENCY_10, const(0x272A)),
125      (REP_MED, FREQUENCY_10, const(0x2721)),
126      (REP_HIGH, FREQUENCY_10, const(0x2737)),
127  )
128  
129  _DELAY = ((REP_LOW, 0.0045), (REP_MED, 0.0065), (REP_HIGH, 0.0155))
130  
131  
132  def _crc(data):
133      crc = 0xFF
134      for byte in data:
135          crc ^= byte
136          for _ in range(8):
137              if crc & 0x80:
138                  crc <<= 1
139                  crc ^= 0x131
140              else:
141                  crc <<= 1
142      return crc
143  
144  
145  def _unpack(data):
146      length = len(data)
147      crc = [None] * (length // 3)
148      word = [None] * (length // 3)
149      for i in range(length // 6):
150          word[i * 2], crc[i * 2], word[(i * 2) + 1], crc[(i * 2) + 1] = struct.unpack(
151              ">HBHB", data[i * 6 : (i * 6) + 6]
152          )
153          if crc[i * 2] == _crc(data[i * 6 : (i * 6) + 2]):
154              length = (i + 1) * 6
155      for i in range(length // 3):
156          if crc[i] != _crc(data[i * 3 : (i * 3) + 2]):
157              raise RuntimeError("CRC mismatch")
158      return word[: length // 3]
159  
160  
161  class SHT31D:
162      """
163      A driver for the SHT31-D temperature and humidity sensor.
164  
165      :param i2c_bus: The `busio.I2C` object to use. This is the only required parameter.
166      :param int address: (optional) The I2C address of the device.
167      """
168  
169      def __init__(self, i2c_bus, address=_SHT31_DEFAULT_ADDRESS):
170          if address not in _SHT31_ADDRESSES:
171              raise ValueError("Invalid address: 0x%x" % (address))
172          self.i2c_device = I2CDevice(i2c_bus, address)
173          self._mode = MODE_SINGLE
174          self._repeatability = REP_HIGH
175          self._frequency = FREQUENCY_4
176          self._clock_stretching = False
177          self._art = False
178          self._last_read = 0
179          self._cached_temperature = None
180          self._cached_humidity = None
181          self._reset()
182  
183      def _command(self, command):
184          with self.i2c_device as i2c:
185              i2c.write(struct.pack(">H", command))
186  
187      def _reset(self):
188          """
189          Soft reset the device
190          The reset command is preceded by a break command as the
191          device will not respond to a soft reset when in 'Periodic' mode.
192          """
193          self._command(_SHT31_PERIODIC_BREAK)
194          time.sleep(0.001)
195          self._command(_SHT31_SOFTRESET)
196          time.sleep(0.0015)
197  
198      def _periodic(self):
199          for command in _PERIODIC_COMMANDS:
200              if self.art == command[0] or (
201                  self.repeatability == command[0] and self.frequency == command[1]
202              ):
203                  self._command(command[2])
204                  time.sleep(0.001)
205                  self._last_read = 0
206  
207      def _data(self):
208          if self.mode == MODE_PERIODIC:
209              data = bytearray(48)
210              data[0] = 0xFF
211              self._command(_SHT31_PERIODIC_FETCH)
212              time.sleep(0.001)
213          elif self.mode == MODE_SINGLE:
214              data = bytearray(6)
215              data[0] = 0xFF
216              for command in _SINGLE_COMMANDS:
217                  if (
218                      self.repeatability == command[0]
219                      and self.clock_stretching == command[1]
220                  ):
221                      self._command(command[2])
222              if not self.clock_stretching:
223                  for delay in _DELAY:
224                      if self.repeatability == delay[0]:
225                          time.sleep(delay[1])
226              else:
227                  time.sleep(0.001)
228          with self.i2c_device as i2c:
229              i2c.readinto(data)
230          word = _unpack(data)
231          length = len(word)
232          temperature = [None] * (length // 2)
233          humidity = [None] * (length // 2)
234          for i in range(length // 2):
235              temperature[i] = -45 + (175 * (word[i * 2] / 65535))
236              humidity[i] = 100 * (word[(i * 2) + 1] / 65535)
237          if (len(temperature) == 1) and (len(humidity) == 1):
238              return temperature[0], humidity[0]
239          return temperature, humidity
240  
241      def _read(self):
242          if (
243              self.mode == MODE_PERIODIC
244              and time.time() > self._last_read + 1 / self.frequency
245          ):
246              self._cached_temperature, self._cached_humidity = self._data()
247              self._last_read = time.time()
248          elif self.mode == MODE_SINGLE:
249              self._cached_temperature, self._cached_humidity = self._data()
250          return self._cached_temperature, self._cached_humidity
251  
252      @property
253      def mode(self):
254          """
255          Operation mode
256          Allowed values are the constants MODE_*
257          Return the device to 'Single' mode to stop periodic data acquisition and allow it to sleep.
258          """
259          return self._mode
260  
261      @mode.setter
262      def mode(self, value):
263          if not value in _SHT31_MODES:
264              raise ValueError("Mode '%s' not supported" % (value))
265          if self._mode == MODE_PERIODIC and value != MODE_PERIODIC:
266              self._command(_SHT31_PERIODIC_BREAK)
267              time.sleep(0.001)
268          if value == MODE_PERIODIC and self._mode != MODE_PERIODIC:
269              self._periodic()
270          self._mode = value
271  
272      @property
273      def repeatability(self):
274          """
275          Repeatability
276          Allowed values are the constants REP_*
277          """
278          return self._repeatability
279  
280      @repeatability.setter
281      def repeatability(self, value):
282          if not value in _SHT31_REP:
283              raise ValueError("Repeatability '%s' not supported" % (value))
284          if self.mode == MODE_PERIODIC and not self._repeatability == value:
285              self._repeatability = value
286              self._periodic()
287          else:
288              self._repeatability = value
289  
290      @property
291      def clock_stretching(self):
292          """
293          Control clock stretching.
294          This feature only affects 'Single' mode.
295          """
296          return self._clock_stretching
297  
298      @clock_stretching.setter
299      def clock_stretching(self, value):
300          self._clock_stretching = bool(value)
301  
302      @property
303      def art(self):
304          """
305          Control accelerated response time
306          This feature only affects 'Periodic' mode.
307          """
308          return self._art
309  
310      @art.setter
311      def art(self, value):
312          if value:
313              self.frequency = FREQUENCY_4
314          if self.mode == MODE_PERIODIC and not self._art == value:
315              self._art = bool(value)
316              self._periodic()
317          else:
318              self._art = bool(value)
319  
320      @property
321      def frequency(self):
322          """
323          Periodic data acquisition frequency
324          Allowed values are the constants FREQUENCY_*
325          Frequency can not be modified when ART is enabled
326          """
327          return self._frequency
328  
329      @frequency.setter
330      def frequency(self, value):
331          if self.art:
332              raise RuntimeError("Frequency locked to '4 Hz' when ART enabled")
333          if not value in _SHT31_FREQUENCIES:
334              raise ValueError(
335                  "Data acquisition frequency '%s Hz' not supported" % (value)
336              )
337          if self.mode == MODE_PERIODIC and not self._frequency == value:
338              self._frequency = value
339              self._periodic()
340          else:
341              self._frequency = value
342  
343      @property
344      def temperature(self):
345          """
346          The measured temperature in degrees celsius.
347          'Single' mode reads and returns the current temperature as a float.
348          'Periodic' mode returns the most recent readings available from the sensor's cache
349          in a FILO list of eight floats. This list is backfilled with with the
350          sensor's maximum output of 130.0 when the sensor is read before the
351          cache is full.
352          """
353          temperature, _ = self._read()
354          return temperature
355  
356      @property
357      def relative_humidity(self):
358          """
359          The measured relative humidity in percent.
360          'Single' mode reads and returns the current humidity as a float.
361          'Periodic' mode returns the most recent readings available from the sensor's cache
362          in a FILO list of eight floats. This list is backfilled with with the
363          sensor's maximum output of 100.01831417975366 when the sensor is read
364          before the cache is full.
365          """
366          _, humidity = self._read()
367          return humidity
368  
369      @property
370      def heater(self):
371          """Control device's internal heater."""
372          return (self.status & 0x2000) != 0
373  
374      @heater.setter
375      def heater(self, value=False):
376          if value:
377              self._command(_SHT31_HEATER_ENABLE)
378              time.sleep(0.001)
379          else:
380              self._command(_SHT31_HEATER_DISABLE)
381              time.sleep(0.001)
382  
383      @property
384      def status(self):
385          """Device status."""
386          data = bytearray(2)
387          self._command(_SHT31_READSTATUS)
388          time.sleep(0.001)
389          with self.i2c_device as i2c:
390              i2c.readinto(data)
391          status = data[0] << 8 | data[1]
392          return status
393  
394      @property
395      def serial_number(self):
396          """Device serial number."""
397          data = bytearray(6)
398          data[0] = 0xFF
399          self._command(_SHT31_READSERIALNBR)
400          time.sleep(0.001)
401          with self.i2c_device as i2c:
402              i2c.readinto(data)
403          word = _unpack(data)
404          return (word[0] << 16) | word[1]