mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	feat: Add HTU31D Support (#5805)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -152,6 +152,7 @@ esphome/components/honeywellabp2_i2c/* @jpfaff | ||||
| esphome/components/host/* @esphome/core | ||||
| esphome/components/hrxl_maxsonar_wr/* @netmikey | ||||
| esphome/components/hte501/* @Stock-M | ||||
| esphome/components/htu31d/* @betterengineering | ||||
| esphome/components/hydreon_rgxx/* @functionpointer | ||||
| esphome/components/hyt271/* @Philippe12 | ||||
| esphome/components/i2c/* @esphome/core | ||||
|   | ||||
							
								
								
									
										1
									
								
								esphome/components/htu31d/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/htu31d/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@betterengineering"] | ||||
							
								
								
									
										269
									
								
								esphome/components/htu31d/htu31d.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								esphome/components/htu31d/htu31d.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,269 @@ | ||||
| /* | ||||
|  * This file contains source code derived from Adafruit_HTU31D which is under | ||||
|  * the BSD license: | ||||
|  *   Written by Limor Fried/Ladyada for Adafruit Industries. | ||||
|  *   BSD license, all text above must be included in any redistribution. | ||||
|  * | ||||
|  * Modifications made by Mark Spicer. | ||||
|  */ | ||||
|  | ||||
| #include "htu31d.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace htu31d { | ||||
|  | ||||
| /** Logging prefix */ | ||||
| static const char *const TAG = "htu31d"; | ||||
|  | ||||
| /** Default I2C address for the HTU31D. */ | ||||
| static const uint8_t HTU31D_DEFAULT_I2CADDR = 0x40; | ||||
|  | ||||
| /** Read temperature and humidity. */ | ||||
| static const uint8_t HTU31D_READTEMPHUM = 0x00; | ||||
|  | ||||
| /** Start a conversion! */ | ||||
| static const uint8_t HTU31D_CONVERSION = 0x40; | ||||
|  | ||||
| /** Read serial number command. */ | ||||
| static const uint8_t HTU31D_READSERIAL = 0x0A; | ||||
|  | ||||
| /** Enable heater */ | ||||
| static const uint8_t HTU31D_HEATERON = 0x04; | ||||
|  | ||||
| /** Disable heater */ | ||||
| static const uint8_t HTU31D_HEATEROFF = 0x02; | ||||
|  | ||||
| /** Reset command. */ | ||||
| static const uint8_t HTU31D_RESET = 0x1E; | ||||
|  | ||||
| /** Diagnostics command. */ | ||||
| static const uint8_t HTU31D_DIAGNOSTICS = 0x08; | ||||
|  | ||||
| /** | ||||
|  * Computes a CRC result for the provided input. | ||||
|  * | ||||
|  * @returns the computed CRC result for the provided input | ||||
|  */ | ||||
| uint8_t compute_crc(uint32_t value) { | ||||
|   uint32_t polynom = 0x98800000;  // x^8 + x^5 + x^4 + 1 | ||||
|   uint32_t msb = 0x80000000; | ||||
|   uint32_t mask = 0xFF800000; | ||||
|   uint32_t threshold = 0x00000080; | ||||
|   uint32_t result = value; | ||||
|  | ||||
|   while (msb != threshold) { | ||||
|     // Check if msb of current value is 1 and apply XOR mask | ||||
|     if (result & msb) | ||||
|       result = ((result ^ polynom) & mask) | (result & ~mask); | ||||
|  | ||||
|     // Shift by one | ||||
|     msb >>= 1; | ||||
|     mask >>= 1; | ||||
|     polynom >>= 1; | ||||
|   } | ||||
|  | ||||
|   return result; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Resets the sensor and ensures that the devices serial number can be read over | ||||
|  * I2C. | ||||
|  */ | ||||
| void HTU31DComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up esphome/components/htu31d HTU31D..."); | ||||
|  | ||||
|   if (!this->reset_()) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (this->read_serial_num_() == 0) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Called once every update interval (user configured, defaults to 60s) and sets | ||||
|  * the current temperature and humidity. | ||||
|  */ | ||||
| void HTU31DComponent::update() { | ||||
|   ESP_LOGD(TAG, "Checking temperature and humidty values"); | ||||
|  | ||||
|   // Trigger a conversion. From the spec sheet: The conversion command triggers | ||||
|   // a single temperature and humidity conversion. | ||||
|   if (this->write_register(HTU31D_CONVERSION, nullptr, 0) != i2c::ERROR_OK) { | ||||
|     this->status_set_warning(); | ||||
|     ESP_LOGE(TAG, "Received errror writing conversion register"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Wait conversion time. | ||||
|   this->set_timeout(20, [this]() { | ||||
|     uint8_t thdata[6]; | ||||
|     if (this->read_register(HTU31D_READTEMPHUM, thdata, 6) != i2c::ERROR_OK) { | ||||
|       this->status_set_warning(); | ||||
|       ESP_LOGE(TAG, "Error reading temperature/humidty register"); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Calculate temperature value. | ||||
|     uint16_t raw_temp = encode_uint16(thdata[0], thdata[1]); | ||||
|  | ||||
|     uint8_t crc = compute_crc((uint32_t) raw_temp << 8); | ||||
|     if (crc != thdata[2]) { | ||||
|       this->status_set_warning(); | ||||
|       ESP_LOGE(TAG, "Error validating temperature CRC"); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     float temperature = raw_temp; | ||||
|     temperature /= 65535.0f; | ||||
|     temperature *= 165; | ||||
|     temperature -= 40; | ||||
|  | ||||
|     if (this->temperature_ != nullptr) { | ||||
|       this->temperature_->publish_state(temperature); | ||||
|     } | ||||
|  | ||||
|     // Calculate humidty value. | ||||
|     uint16_t raw_hum = encode_uint16(thdata[3], thdata[4]); | ||||
|  | ||||
|     crc = compute_crc((uint32_t) raw_hum << 8); | ||||
|     if (crc != thdata[5]) { | ||||
|       this->status_set_warning(); | ||||
|       ESP_LOGE(TAG, "Error validating humidty CRC"); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     float humidity = raw_hum; | ||||
|     humidity /= 65535.0f; | ||||
|     humidity *= 100; | ||||
|  | ||||
|     if (this->humidity_ != nullptr) { | ||||
|       this->humidity_->publish_state(humidity); | ||||
|     } | ||||
|  | ||||
|     ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity); | ||||
|     this->status_clear_warning(); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Logs the current compoenent config. | ||||
|  */ | ||||
| void HTU31DComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "HTU31D:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, "Communication with HTU31D failed!"); | ||||
|   } | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_); | ||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Sends a 'reset' request to the HTU31D, followed by a 15ms delay. | ||||
|  * | ||||
|  * @returns True if was able to write the command successfully | ||||
|  */ | ||||
| bool HTU31DComponent::reset_() { | ||||
|   if (this->write_register(HTU31D_RESET, nullptr, 0) != i2c::ERROR_OK) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   delay(15); | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Reads the serial number from the device and checks the CRC. | ||||
|  * | ||||
|  * @returns the 24bit serial number from the device | ||||
|  */ | ||||
| uint32_t HTU31DComponent::read_serial_num_() { | ||||
|   uint8_t reply[4]; | ||||
|   uint32_t serial = 0; | ||||
|   uint8_t padding = 0; | ||||
|  | ||||
|   // Verify we can read the device serial. | ||||
|   if (this->read_register(HTU31D_READSERIAL, reply, 4) != i2c::ERROR_OK) { | ||||
|     ESP_LOGE(TAG, "Error reading device serial"); | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   serial = encode_uint32(reply[0], reply[1], reply[2], padding); | ||||
|  | ||||
|   uint8_t crc = compute_crc(serial); | ||||
|   if (crc != reply[3]) { | ||||
|     ESP_LOGE(TAG, "Error validating serial CRC"); | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Found serial: 0x%X", serial); | ||||
|  | ||||
|   return serial; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Checks the diagnostics register to determine if the heater is currently | ||||
|  * enabled. | ||||
|  * | ||||
|  * @returns True if the heater is currently enabled, False otherwise | ||||
|  */ | ||||
| bool HTU31DComponent::is_heater_enabled() { | ||||
|   uint8_t reply[1]; | ||||
|   uint8_t heater_enabled_position = 0; | ||||
|   uint8_t mask = 1 << heater_enabled_position; | ||||
|   uint8_t diagnostics = 0; | ||||
|  | ||||
|   if (this->read_register(HTU31D_DIAGNOSTICS, reply, 1) != i2c::ERROR_OK) { | ||||
|     ESP_LOGE(TAG, "Error reading device serial"); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   diagnostics = reply[0]; | ||||
|   return (diagnostics & mask) != 0; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Sets the heater state on or off. | ||||
|  * | ||||
|  * @param desired True for on, and False for off. | ||||
|  */ | ||||
| void HTU31DComponent::set_heater_state(bool desired) { | ||||
|   bool current = this->is_heater_enabled(); | ||||
|  | ||||
|   // If the current state matches the desired state, there is nothing to do. | ||||
|   if (current == desired) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Update heater state. | ||||
|   esphome::i2c::ErrorCode err; | ||||
|   if (desired) { | ||||
|     err = this->write_register(HTU31D_HEATERON, nullptr, 0); | ||||
|   } else { | ||||
|     err = this->write_register(HTU31D_HEATEROFF, nullptr, 0); | ||||
|   } | ||||
|  | ||||
|   // Record any error. | ||||
|   if (err != i2c::ERROR_OK) { | ||||
|     this->status_set_warning(); | ||||
|     ESP_LOGE(TAG, "Received error updating heater state"); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Sets the startup priority for this component. | ||||
|  * | ||||
|  * @returns The startup priority | ||||
|  */ | ||||
| float HTU31DComponent::get_setup_priority() const { return setup_priority::DATA; } | ||||
| }  // namespace htu31d | ||||
| }  // namespace esphome | ||||
							
								
								
									
										33
									
								
								esphome/components/htu31d/htu31d.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/htu31d/htu31d.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace htu31d { | ||||
|  | ||||
| class HTU31DComponent : public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void setup() override;        /// Setup (reset) the sensor and check connection. | ||||
|   void update() override;       /// Update the sensor values (temperature+humidity). | ||||
|   void dump_config() override;  /// Dumps the configuration values. | ||||
|  | ||||
|   void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } | ||||
|   void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } | ||||
|  | ||||
|   void set_heater_state(bool desired); | ||||
|   bool is_heater_enabled(); | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|  protected: | ||||
|   bool reset_(); | ||||
|   uint32_t read_serial_num_(); | ||||
|  | ||||
|   sensor::Sensor *temperature_{nullptr}; | ||||
|   sensor::Sensor *humidity_{nullptr}; | ||||
| }; | ||||
| }  // namespace htu31d | ||||
| }  // namespace esphome | ||||
							
								
								
									
										56
									
								
								esphome/components/htu31d/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								esphome/components/htu31d/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c, sensor | ||||
| from esphome.const import ( | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_ID, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_PERCENT, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| htu31d_ns = cg.esphome_ns.namespace("htu31d") | ||||
| HTU31DComponent = htu31d_ns.class_( | ||||
|     "HTU31DComponent", cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(HTU31DComponent), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x40)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     if temperature_config := config.get(CONF_TEMPERATURE): | ||||
|         sens = await sensor.new_sensor(temperature_config) | ||||
|         cg.add(var.set_temperature(sens)) | ||||
|  | ||||
|     if humidity_config := config.get(CONF_HUMIDITY): | ||||
|         sens = await sensor.new_sensor(humidity_config) | ||||
|         cg.add(var.set_humidity(sens)) | ||||
							
								
								
									
										9
									
								
								tests/components/htu31d/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								tests/components/htu31d/common.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| i2c: | ||||
|  | ||||
| sensor: | ||||
|   - platform: htu31d | ||||
|     temperature: | ||||
|       name: Living Room Temperature 7 | ||||
|     humidity: | ||||
|       name: Living Room Humidity 7 | ||||
|     update_interval: 15s | ||||
							
								
								
									
										1
									
								
								tests/components/htu31d/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/htu31d/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <<: !include common.yaml | ||||
							
								
								
									
										1
									
								
								tests/components/htu31d/test.esp32.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/htu31d/test.esp32.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <<: !include common.yaml | ||||
							
								
								
									
										1
									
								
								tests/components/htu31d/test.esp8266.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/htu31d/test.esp8266.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <<: !include common.yaml | ||||
		Reference in New Issue
	
	Block a user