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!

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.