From 713b50969848910d52c7d91dce695140bb59df9d Mon Sep 17 00:00:00 2001
From: Timofey Titovets <nefelim4ag@gmail.com>
Date: Sun, 21 Apr 2024 00:42:31 +0200
Subject: [PATCH] sht3x: Add sht31 support (#6560)

Signed-off-by: Timofey Titovets <nefelim4ag@gmail.com>
---
 docs/Config_Reference.md              |  19 +++
 docs/Status_Reference.md              |   3 +-
 klippy/extras/sht3x.py                | 165 ++++++++++++++++++++++++++
 klippy/extras/temperature_sensors.cfg |   2 +
 4 files changed, 188 insertions(+), 1 deletion(-)
 create mode 100644 klippy/extras/sht3x.py

diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md
index 403dfdfc4..71cdfed87 100644
--- a/docs/Config_Reference.md
+++ b/docs/Config_Reference.md
@@ -2575,6 +2575,25 @@ sensor_type:
 #   Interval in seconds between readings. Default is 30
 ```
 
+### SHT3X sensor
+
+SHT3X family two wire interface (I2C) environmental sensor. These sensors
+have a range of -55~125 C, so are usable for e.g. chamber temperature
+monitoring. They can also function as simple fan/heater controllers.
+
+```
+sensor_type: SHT3X
+#i2c_address:
+#   Default is 68 (0x44).
+#i2c_mcu:
+#i2c_bus:
+#i2c_software_scl_pin:
+#i2c_software_sda_pin:
+#i2c_speed:
+#   See the "common I2C settings" section for a description of the
+#   above parameters.
+```
+
 ### LM75 temperature sensor
 
 LM75/LM75A two wire (I2C) connected temperature sensors. These sensors
diff --git a/docs/Status_Reference.md b/docs/Status_Reference.md
index 0e72a12b1..66d840d16 100644
--- a/docs/Status_Reference.md
+++ b/docs/Status_Reference.md
@@ -445,6 +445,7 @@ The following information is available in
 
 [bme280 config_section_name](Config_Reference.md#bmp280bme280bme680-temperature-sensor),
 [htu21d config_section_name](Config_Reference.md#htu21d-sensor),
+[sht3x config_section_name](Config_Reference.md#sht31-sensor),
 [lm75 config_section_name](Config_Reference.md#lm75-temperature-sensor),
 [temperature_host config_section_name](Config_Reference.md#host-temperature-sensor)
 and
@@ -452,7 +453,7 @@ and
 objects:
 - `temperature`: The last read temperature from the sensor.
 - `humidity`, `pressure`, `gas`: The last read values from the sensor
-  (only on bme280, htu21d, and lm75 sensors).
+  (only on bme280, htu21d, sht3x and lm75 sensors).
 
 ## temperature_fan
 
diff --git a/klippy/extras/sht3x.py b/klippy/extras/sht3x.py
new file mode 100644
index 000000000..699d3f209
--- /dev/null
+++ b/klippy/extras/sht3x.py
@@ -0,0 +1,165 @@
+# SHT3X i2c based temperature sensors support
+#
+# Copyright (C) 2024  Timofey Titovets <nefelim4ag@gmail.com>
+# Based on htu21d.py code
+#
+# This file may be distributed under the terms of the GNU GPLv3 license.
+import logging
+from . import bus
+
+######################################################################
+# Compatible Sensors:
+#       SHT31  - Tested on octopus pro and Linux MCU
+#
+######################################################################
+SHT3X_I2C_ADDR = 0x44
+
+SHT3X_CMD = {
+    'MEASURE': {
+        'STRETCH_ENABLED': {
+            'HIGH_REP': [0x2c, 0x06], # High   (15ms) repeatability measurement
+            'MED_REP': [0x2c, 0x0D],  # Medium (6ms)  repeatability measurement
+            'LOW_REP': [0x2c, 0x10],  # Low    (4ms)  repeatability measurement
+        },
+        'STRETCH_DISABLED' : {
+            'HIGH_REP': [0x24, 0x00],
+            'MED_REP': [0x24, 0x0B],
+            'LOW_REP': [0x24, 0x16],
+        },
+    },
+    'OTHER': {
+        'STATUS': {
+            'READ': [0xF3, 0x2D],
+            'CLEAN': [0x30, 0x41],
+        },
+        'SOFTRESET': [0x30, 0xA2], # Soft reset
+        'HEATER': {
+            "ENABLE": [0x30, 0x6D],
+            "DISABLE": [0x30, 0x66],
+        },
+        'FETCH': [0xE0, 0x00],
+        'BREAK': [0x30, 0x93],
+    }
+}
+
+class SHT3X:
+    def __init__(self, config):
+        self.printer = config.get_printer()
+        self.name = config.get_name().split()[-1]
+        self.reactor = self.printer.get_reactor()
+        self.i2c = bus.MCU_I2C_from_config(
+            config, default_addr=SHT3X_I2C_ADDR, default_speed=100000)
+        self.report_time = config.getint('sht3x_report_time', 1, minval=1)
+        self.deviceId = config.get('sensor_type')
+        self.temp = self.min_temp = self.max_temp = self.humidity = 0.
+        self.sample_timer = self.reactor.register_timer(self._sample_sht3x)
+        self.printer.add_object("sht3x " + self.name, self)
+        self.printer.register_event_handler("klippy:connect",
+                                            self.handle_connect)
+    def handle_connect(self):
+        self._init_sht3x()
+        self.reactor.update_timer(self.sample_timer, self.reactor.NOW)
+
+    def setup_minmax(self, min_temp, max_temp):
+        self.min_temp = min_temp
+        self.max_temp = max_temp
+
+    def setup_callback(self, cb):
+        self._callback = cb
+
+    def get_report_time_delta(self):
+        return self.report_time
+
+    def _init_sht3x(self):
+        # Device Soft Reset
+        self.i2c.i2c_write(SHT3X_CMD['OTHER']['SOFTRESET'])
+
+        # Wait 2ms after reset
+        self.reactor.pause(self.reactor.monotonic() + .02)
+
+        status = self.i2c.i2c_read(SHT3X_CMD['OTHER']['STATUS']['READ'], 3)
+        response = bytearray(status['response'])
+        status = response[0] << 8
+        status |= response[1]
+        checksum = response[2]
+
+        if self._crc8(status) != checksum:
+            logging.warning("sht3x: Reading status - checksum error!")
+
+    def _sample_sht3x(self, eventtime):
+        try:
+            # Read Temeprature
+            params = self.i2c.i2c_write(
+                SHT3X_CMD['MEASURE']['STRETCH_ENABLED']['HIGH_REP']
+            )
+            # Wait
+            self.reactor.pause(self.reactor.monotonic()
+            + .20)
+
+            params = self.i2c.i2c_read([], 6)
+
+            response = bytearray(params['response'])
+            rtemp  = response[0] << 8
+            rtemp |= response[1]
+            if self._crc8(rtemp) != response[2]:
+                logging.warning(
+                    "sht3x: Checksum error on Temperature reading!"
+                )
+            else:
+                self.temp = -45 + (175 * rtemp / 65535)
+                logging.debug("sht3x: Temperature %.2f " % self.temp)
+
+            rhumid  = response[3] << 8
+            rhumid |= response[4]
+            if self._crc8(rhumid) != response[5]:
+                logging.warning("sht3x: Checksum error on Humidity reading!")
+            else:
+                self.humidity = 100 * rhumid / 65535
+                logging.debug("sht3x: Humidity %.2f " % self.humidity)
+
+        except Exception:
+            logging.exception("sht3x: Error reading data")
+            self.temp = self.humidity = .0
+            return self.reactor.NEVER
+
+        if self.temp < self.min_temp or self.temp > self.max_temp:
+            self.printer.invoke_shutdown(
+                "sht3x: temperature %0.1f outside range of %0.1f:%.01f"
+                % (self.temp, self.min_temp, self.max_temp))
+
+        measured_time = self.reactor.monotonic()
+        print_time = self.i2c.get_mcu().estimated_print_time(measured_time)
+        self._callback(print_time, self.temp)
+        return measured_time + self.report_time
+
+    def _split_bytes(self, data):
+        bytes = []
+        for i in range((data.bit_length() + 7) // 8):
+            bytes.append((data >> i*8) & 0xFF)
+        bytes.reverse()
+        return bytes
+
+    def _crc8(self, data):
+        #crc8 polynomial for 16bit value, CRC8 -> x^8 + x^5 + x^4 + 1
+        SHT3X_CRC8_POLYNOMINAL= 0x31
+        crc = 0xFF
+        data_bytes = self._split_bytes(data)
+        for byte in data_bytes:
+            crc ^= byte
+            for _ in range(8):
+                if crc & 0x80:
+                    crc = (crc << 1) ^ SHT3X_CRC8_POLYNOMINAL
+                else:
+                    crc <<= 1
+        return crc & 0xFF
+
+    def get_status(self, eventtime):
+        return {
+            'temperature': round(self.temp, 2),
+            'humidity': round(self.humidity, 1),
+        }
+
+def load_config(config):
+    # Register sensor
+    pheater = config.get_printer().lookup_object("heaters")
+    pheater.add_sensor_factory("SHT3X", SHT3X)
diff --git a/klippy/extras/temperature_sensors.cfg b/klippy/extras/temperature_sensors.cfg
index 107fcd24b..4fbe5492c 100644
--- a/klippy/extras/temperature_sensors.cfg
+++ b/klippy/extras/temperature_sensors.cfg
@@ -18,6 +18,8 @@
 # Load "SI7013", "SI7020", "SI7021", "SHT21", and "HTU21D" sensors
 [htu21d]
 
+[sht3x]
+
 # Load "AHT10"
 [aht10]