Support for Sensirion SHT3X temp RH sensor

Hello, I am trying to write an extras module to add support for Sensirion SHT3x temperature and humidity sensors, but I’m having trouble getting Klipper to recognize the module as a temperature sensor. I’ve been using the AHT10 and HTU21D modules as a guide, but I’ve hit a roadblock. Admittedly, I don’t have a whole lot of experience with this sort of thing. Any help would be much appreciated.

Klipper reports: ERROR
Unknown temperature sensor ‘SHT30’

Here’s my code, saved in sht30.py

# SHT30 i2c based temperature sensors support
# Clock stretching is not implemented.  Oneshot measurements only.

import logging
from . import bus

SHT30_I2C_ADDR = 0x44

SHT30_COMMANDS = {
    'SHT30_STATUS_CMD'      :[0xF3,0x2D],
    'SHT30_CLEAR_STATUS_CMD':[0x30,0x41],
    'SHT30_RESET_CMD'       :[0x30,0xA2],
    'SINGLE_SHOT_H'         :[0x24,0x00],
    'SINGLE_SHOT_M'         :[0x24,0x0B],
    'SINGLE_SHOT_L'         :[0x24,0x16]
    }

# Measurement duration for each repeatability setting (seconds)
SHT30_MEAS_DURATION_REP = {
    'high'              :0.015,   
    'medium'            :0.006,
    'low'               :0.004
    }

SHT30_STATUS_BITS = {
    'ALERT_PENDING'         :0x8000,    # Bit 15: 0: No alerts; 1: Alerts pending
    'HEATER_STATUS'         :0x2000,    # Bit 13: 0: Off; 1: On 
    'RH_TRACKING_ALERT'     :0x0800,    # Bit 11: 0: No alert; 1: Alert
    'T_TRACKING_ALERT'      :0x0400,    # Bit 10: 0: No alert; 1: Alert
    'SYSTEM_RESET_DET'      :0x0010,    # Bit 4: 0: no rst since last clear; 1: rst detected
    'COMMAND_STATUS'        :0x0002,    # Bit 1: 0: success; 1: failed
    'WRITE_CHECKSUM_STATUS' :0x0001     # Bit 0: 0: correct; 1: failed
}

#crc8 polynomial for 16bit value, CRC8 -> x^8 + x^5 + x^4 + 1
SHT30_CRC8_POLYNOMINAL= 0x31

class SHT30:
    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=SHT30_I2C_ADDR, default_speed=100000)
        self.repeatability = config.get('sht30_repeatability','medium')
        self.report_time = config.getint('sht30_report_time',30,minval=5)
        if self.repeatability not in SHT30_MEAS_DURATION_REP:
            raise config.error("Invalid SHT30 Repeatability. Valid are %s"
                % '|'.join(SHT30_MEAS_DURATION_REP.keys()))
        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_sht30)
        self.printer.add_object("sht30 " + self.name, self)
        self.printer.register_event_handler("klippy:connect", self.handle_connect)

    def handle_connect(self):
        self._init_sht30()
        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_sht30(self):
        # Device Soft Reset
        self.i2c.i2c_write([SHT30_COMMANDS['SHT30_RESET_CMD']])
        # Wait 1.5ms after reset
        self.reactor.pause(self.reactor.monotonic() + 0.0015)
        # Get status
        params = self.i2c.i2c_read([SHT30_COMMANDS['SHT30_STATUS_CMD']], 3)
        response = bytearray(params['response'])
        devStatus = response[0] << 8
        devStatus |= response[1]
        checksum = response[2]
        if self._chekCRC8(devStatus) != checksum:
            logging.warning("sht30: Reading device status !Checksum error!")
        if devStatus & [SHT30_STATUS_BITS['COMMAND_STATUS']] == [SHT30_STATUS_BITS['COMMAND_STATUS']]:
            logging.info("sht30: Reset command successful")
        else:
            logging.warning("sht30: Reset command failed")
        # Clear status
        self.i2c.i2c_write([SHT30_COMMANDS['SHT30_CLEAR_STATUS_CMD']])
        logging.info("sht30: Clearing status bits")
        logging.info("sht30: Repeatability set to %s " % self.repeatability)

    def _sample_sht30(self, eventtime):
        try:
            # Read Temeprature and RH
            if self.repeatability == 'high':
                params = self.i2c.i2c_write([SHT30_COMMMANDS['SINGLE_SHOT_H']])
            elif self.repeatability == 'medium':
                params = self.i2c.i2c_write([SHT30_COMMANDS['SINGLE_SHOT_M']])
            elif self.repeatability == 'low':
                params = self.i2c.i2c_write([SHT30_COMMANDS['SINGLE_SHOT_L']])
            else:
                params = self.i2c.i2c_write([SHT30_COMMANDS['SINGLE_SHOT_M']])

            # Wait
            self.reactor.pause(self.reactor.monotonic() + SHT30_MEAS_DURATION_REP[self.repeatability])

            # Read response
            params = self.i2c.i2c_read([],6)

            response = bytearray(params['response'])
            
            #Bytes 0 and 1 are temperature. Byte 2 is CRC checksum.
            rtemp  = response[0] << 8
            rtemp |= response[1]
            if self._chekCRC8(rtemp) != response[2]:
                logging.warning(
                    "sht30: Checksum error on Temperature reading!"
                )
            else:
                # Temp conversion to degree C
                self.temp = (175.0 * float(rtemp) / (2 ** 16 - 1) - 45.0)
                logging.debug("sht30: Temperature %.2f " % self.temp)

            #Bytes 3 and 4 are relative humidity. Byte 5 is CRC checksum. 
            rhumid = response[3] << 8
            rhumid|= response[4]
            if self._chekCRC8(rhumid) != response[5]:
                logging.warning("sht30: Checksum error on Humidity reading!")
            else:
                self.humidity = (100.0 * float(rhumid) / (2 ** 16 - 1))
                if (self.humidity < 0):
                    #due to RH accuracy, measured value might be
                    # slightly less than 0 or more 100
                    self.humidity = 0
                elif (self.humidity > 100):
                    self.humidity = 100
                logging.debug("sht30: Humidity %.2f " % self.humidity)
        except Exception:
            logging.exception("sht30: Error reading data")
            self.temp = self.humidity = .0
            return self.reactor.NEVER

        if self.temp < self.min_temp or self.temp > self.max_temp:
            logging.exception("SHT30 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 _chekCRC8(self,data):
        for bit in range(0,16):
            if (data & 0x8000):
                data = (data << 1) ^ SHT30_CRC8_POLYNOMINAL;
            else:
                data <<= 1
        data = data >> 8
        return data

    def get_status(self, eventtime):
        return {
            'temperature': round(self.temp, 2),
            'humidity': self.humidity,
        }

def load_config(config):
    # Register sensor
    pheater = config.get_printer().lookup_object("heaters")
    pheater.add_sensor_factory("SHT30", SHT30)

From my printer.cfg

[mcu rpi]
serial: /tmp/klipper_host_mcu

[temperature_sensor Enclosure]
sensor_type: SHT30
i2c_address: 44
i2c_mcu: rpi
i2c_bus: i2c.1
sht3x_repeatability: medium
sht3X_report_time: 30
min_temp: 0
max_temp: 100

from klippy.log

=======================
Config error
Traceback (most recent call last):
  File "/home/jpkramm/klipper/klippy/klippy.py", line 175, in _connect
    self._read_config()
  File "/home/jpkramm/klipper/klippy/klippy.py", line 141, in _read_config
    self.load_object(config, section_config.get_name(), None)
  File "/home/jpkramm/klipper/klippy/klippy.py", line 130, in load_object
    self.objects[section] = init_func(config.getsection(section))
  File "/home/jpkramm/klipper/klippy/extras/temperature_sensor.py", line 42, in load_config_prefix
    return PrinterSensorGeneric(config)
  File "/home/jpkramm/klipper/klippy/extras/temperature_sensor.py", line 14, in __init__
    self.sensor = pheaters.setup_sensor(config)
  File "/home/jpkramm/klipper/klippy/extras/heaters.py", line 282, in setup_sensor
    raise self.printer.config_error(
configparser.Error: Unknown temperature sensor 'SHT30'
webhooks client 4131893288: New connection
webhooks client 4131893288: Client info {'program': 'Moonraker', 'version': 'v0.8.0-317-g0850c16'}

Update: I figured out that I needed to add a temperature sensor type to
/klippy/extras/temperature_sensors.cfg.

Still errors out, but a step in the right direction.

1 Like

I have several SHT3x sensors which were used in my biz. project together with STM microcontroller.
But because I am not familiar with Klipper on the level what I can see from your publication, if you can share a bit more info where is the location for your sht30.py file and how it can be connected with the klipper, I might have a look and do the analysis on my side.

I have CR10S Pro with BIQU SKR mini E3 v3 motherboard + Creality SonicPad. This is not a convenient configuration for this type development, but I already implemented full support of this sensor in my STM microcontroller code, so, may be it can help…

I added SHT31 at least, according to the datasheet others from this family must also work

Fantastic, thanks!