diff --git a/.gitignore b/.gitignore index fa4670769b..2de9dd40e5 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ htmlcov/ .coverage .coverage.* .cache +.esphome nosetests.xml coverage.xml *.cover diff --git a/esphome/components/max31856/__init__.py b/esphome/components/max31856/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp new file mode 100644 index 0000000000..b3e5b9277a --- /dev/null +++ b/esphome/components/max31856/max31856.cpp @@ -0,0 +1,198 @@ +#include "max31856.h" + +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace max31856 { + +static const char *TAG = "max31856"; + +// Based on Adafruit's library: https://github.com/adafruit/Adafruit_MAX31856 + +void MAX31856Sensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX31856Sensor '%s'...", this->name_.c_str()); + this->spi_setup(); + + // assert on any fault + ESP_LOGCONFIG(TAG, "Setting up assertion on all faults"); + this->write_register_(MAX31856_MASK_REG, 0x0); + + ESP_LOGCONFIG(TAG, "Setting up open circuit fault detection"); + this->write_register_(MAX31856_CR0_REG, MAX31856_CR0_OCFAULT01); + + this->set_thermocouple_type_(); + this->set_noise_filter_(); + + ESP_LOGCONFIG(TAG, "Completed setting up MAX31856Sensor '%s'...", this->name_.c_str()); +} + +void MAX31856Sensor::dump_config() { + LOG_SENSOR("", "MAX31856", this); + LOG_PIN(" CS Pin: ", this->cs_); + ESP_LOGCONFIG(TAG, " Mains Filter: %s", + (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); + LOG_UPDATE_INTERVAL(this); +} + +void MAX31856Sensor::update() { + ESP_LOGVV(TAG, "update"); + + this->one_shot_temperature_(); + + // Datasheet max conversion time for 1 shot is 155ms for 60Hz / 185ms for 50Hz + auto f = std::bind(&MAX31856Sensor::read_thermocouple_temperature_, this); + this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185, f); +} + +void MAX31856Sensor::read_thermocouple_temperature_() { + if (this->has_fault_()) { + // Faults have been logged, clear it for next loop + this->clear_fault_(); + } else { + int32_t temp24 = this->read_register24_(MAX31856_LTCBH_REG); + if (temp24 & 0x800000) { + temp24 |= 0xFF000000; // fix sign + } + + temp24 >>= 5; // bottom 5 bits are unused + + float temp_c = temp24; + temp_c *= 0.0078125; + + ESP_LOGD(TAG, "Got thermocouple temperature: %.2f°C", temp_c); + this->publish_state(temp_c); + } +} + +void MAX31856Sensor::one_shot_temperature_() { + ESP_LOGVV(TAG, "one_shot_temperature_"); + this->write_register_(MAX31856_CJTO_REG, 0x0); + + uint8_t t = this->read_register_(MAX31856_CR0_REG); + + t &= ~MAX31856_CR0_AUTOCONVERT; // turn off autoconversion mode + t |= MAX31856_CR0_1SHOT; // turn on one shot mode + + this->write_register_(MAX31856_CR0_REG, t); +} + +bool MAX31856Sensor::has_fault_() { + ESP_LOGVV(TAG, "read_fault_"); + uint8_t faults = this->read_register_(MAX31856_SR_REG); + + if (faults == 0) { + ESP_LOGV(TAG, "status_set_warning"); + this->status_clear_warning(); + return false; + } + + ESP_LOGV(TAG, "status_set_warning"); + this->status_set_warning(); + + if ((faults & MAX31856_FAULT_CJRANGE) == MAX31856_FAULT_CJRANGE) { + ESP_LOGW(TAG, "Cold Junction Out-of-Range: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_TCRANGE) == MAX31856_FAULT_TCRANGE) { + ESP_LOGW(TAG, "Thermocouple Out-of-Range: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_CJHIGH) == MAX31856_FAULT_CJHIGH) { + ESP_LOGW(TAG, "Cold-Junction High Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_CJLOW) == MAX31856_FAULT_CJLOW) { + ESP_LOGW(TAG, "Cold-Junction Low Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_TCHIGH) == MAX31856_FAULT_TCHIGH) { + ESP_LOGW(TAG, "Thermocouple Temperature High Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_TCLOW) == MAX31856_FAULT_TCLOW) { + ESP_LOGW(TAG, "Thermocouple Temperature Low Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_OVUV) == MAX31856_FAULT_OVUV) { + ESP_LOGW(TAG, "Overvoltage or Undervoltage Input Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_OPEN) == MAX31856_FAULT_OPEN) { + ESP_LOGW(TAG, "Thermocouple Open-Circuit Fault (possibly not connected): '%s'...", this->name_.c_str()); + } + + return true; +} + +void MAX31856Sensor::clear_fault_() { + ESP_LOGV(TAG, "clear_fault_"); + uint8_t t = this->read_register_(MAX31856_CR0_REG); + + t |= MAX31856_CR0_FAULT; // turn on fault interrupt mode + t |= MAX31856_CR0_FAULTCLR; // enable the fault status clear bit + + this->write_register_(MAX31856_CR0_REG, t); +} + +void MAX31856Sensor::set_thermocouple_type_() { + MAX31856ThermocoupleType type = MAX31856_TCTYPE_K; + ESP_LOGCONFIG(TAG, "set_thermocouple_type_: 0x%02X", type); + uint8_t t = this->read_register_(MAX31856_CR1_REG); + t &= 0xF0; // mask off bottom 4 bits + t |= (uint8_t) type & 0x0F; + this->write_register_(MAX31856_CR1_REG, t); +} + +void MAX31856Sensor::set_noise_filter_() { + ESP_LOGCONFIG(TAG, "set_noise_filter_: 0x%02X", filter_); + uint8_t t = this->read_register_(MAX31856_CR0_REG); + if (filter_ == FILTER_50HZ) { + t |= 0x01; + ESP_LOGCONFIG(TAG, "set_noise_filter_: 50 Hz, t==0x%02X", t); + } else { + t &= 0xfe; + ESP_LOGCONFIG(TAG, "set_noise_filter_: 60 Hz, t==0x%02X", t); + } + this->write_register_(MAX31856_CR0_REG, t); +} + +void MAX31856Sensor::write_register_(uint8_t reg, uint8_t value) { + ESP_LOGVV(TAG, "write_register_ raw reg=0x%02X value=0x%02X", reg, value); + reg |= SPI_WRITE_M; + ESP_LOGVV(TAG, "write_register_ masked reg=0x%02X value=0x%02X", reg, value); + this->enable(); + ESP_LOGVV(TAG, "write_byte reg=0x%02X", reg); + this->write_byte(reg); + ESP_LOGVV(TAG, "write_byte value=0x%02X", value); + this->write_byte(value); + this->disable(); + ESP_LOGV(TAG, "write_register_ 0x%02X: 0x%02X", reg, value); +} + +const uint8_t MAX31856Sensor::read_register_(uint8_t reg) { + ESP_LOGVV(TAG, "read_register_ 0x%02X", reg); + this->enable(); + ESP_LOGVV(TAG, "write_byte reg=0x%02X", reg); + this->write_byte(reg); + const uint8_t value(this->read_byte()); + ESP_LOGVV(TAG, "read_byte value=0x%02X", value); + this->disable(); + ESP_LOGV(TAG, "read_register_ reg=0x%02X: value=0x%02X", reg, value); + return value; +} + +const uint32_t MAX31856Sensor::read_register24_(uint8_t reg) { + ESP_LOGVV(TAG, "read_register_24_ 0x%02X", reg); + this->enable(); + ESP_LOGVV(TAG, "write_byte reg=0x%02X", reg); + this->write_byte(reg); + const uint8_t msb(this->read_byte()); + ESP_LOGVV(TAG, "read_byte msb=0x%02X", msb); + const uint8_t mid(this->read_byte()); + ESP_LOGVV(TAG, "read_byte mid=0x%02X", mid); + const uint8_t lsb(this->read_byte()); + ESP_LOGVV(TAG, "read_byte lsb=0x%02X", lsb); + this->disable(); + const uint32_t value((msb << 16) | (mid << 8) | lsb); + ESP_LOGV(TAG, "read_register_24_ reg=0x%02X: value=0x%06X", reg, value); + return value; +} + +float MAX31856Sensor::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace max31856 +} // namespace esphome diff --git a/esphome/components/max31856/max31856.h b/esphome/components/max31856/max31856.h new file mode 100644 index 0000000000..779eb52c8e --- /dev/null +++ b/esphome/components/max31856/max31856.h @@ -0,0 +1,98 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace max31856 { + +enum MAX31856RegisterMasks { SPI_WRITE_M = 0x80 }; + +enum MAX31856Registers { + MAX31856_CR0_REG = 0x00, ///< Config 0 register + MAX31856_CR0_AUTOCONVERT = 0x80, ///< Config 0 Auto convert flag + MAX31856_CR0_1SHOT = 0x40, ///< Config 0 one shot convert flag + MAX31856_CR0_OCFAULT00 = 0x00, ///< Config 0 open circuit fault 00 flag + MAX31856_CR0_OCFAULT01 = 0x10, ///< Config 0 open circuit fault 01 flag + MAX31856_CR0_OCFAULT10 = 0x20, ///< Config 0 open circuit fault 10 flag + MAX31856_CR0_CJ = 0x08, ///< Config 0 cold junction disable flag + MAX31856_CR0_FAULT = 0x04, ///< Config 0 fault mode flag + MAX31856_CR0_FAULTCLR = 0x02, ///< Config 0 fault clear flag + + MAX31856_CR1_REG = 0x01, ///< Config 1 register + MAX31856_MASK_REG = 0x02, ///< Fault Mask register + MAX31856_CJHF_REG = 0x03, ///< Cold junction High temp fault register + MAX31856_CJLF_REG = 0x04, ///< Cold junction Low temp fault register + MAX31856_LTHFTH_REG = 0x05, ///< Linearized Temperature High Fault Threshold Register, MSB + MAX31856_LTHFTL_REG = 0x06, ///< Linearized Temperature High Fault Threshold Register, LSB + MAX31856_LTLFTH_REG = 0x07, ///< Linearized Temperature Low Fault Threshold Register, MSB + MAX31856_LTLFTL_REG = 0x08, ///< Linearized Temperature Low Fault Threshold Register, LSB + MAX31856_CJTO_REG = 0x09, ///< Cold-Junction Temperature Offset Register + MAX31856_CJTH_REG = 0x0A, ///< Cold-Junction Temperature Register, MSB + MAX31856_CJTL_REG = 0x0B, ///< Cold-Junction Temperature Register, LSB + MAX31856_LTCBH_REG = 0x0C, ///< Linearized TC Temperature, Byte 2 + MAX31856_LTCBM_REG = 0x0D, ///< Linearized TC Temperature, Byte 1 + MAX31856_LTCBL_REG = 0x0E, ///< Linearized TC Temperature, Byte 0 + MAX31856_SR_REG = 0x0F, ///< Fault Status Register + + MAX31856_FAULT_CJRANGE = 0x80, ///< Fault status Cold Junction Out-of-Range flag + MAX31856_FAULT_TCRANGE = 0x40, ///< Fault status Thermocouple Out-of-Range flag + MAX31856_FAULT_CJHIGH = 0x20, ///< Fault status Cold-Junction High Fault flag + MAX31856_FAULT_CJLOW = 0x10, ///< Fault status Cold-Junction Low Fault flag + MAX31856_FAULT_TCHIGH = 0x08, ///< Fault status Thermocouple Temperature High Fault flag + MAX31856_FAULT_TCLOW = 0x04, ///< Fault status Thermocouple Temperature Low Fault flag + MAX31856_FAULT_OVUV = 0x02, ///< Fault status Overvoltage or Undervoltage Input Fault flag + MAX31856_FAULT_OPEN = 0x01, ///< Fault status Thermocouple Open-Circuit Fault flag +}; + +/** + * Multiple types of thermocouples supported by the chip. + * Currently only K type implemented here. + */ +enum MAX31856ThermocoupleType { + MAX31856_TCTYPE_B = 0b0000, // 0x00 + MAX31856_TCTYPE_E = 0b0001, // 0x01 + MAX31856_TCTYPE_J = 0b0010, // 0x02 + MAX31856_TCTYPE_K = 0b0011, // 0x03 + MAX31856_TCTYPE_N = 0b0100, // 0x04 + MAX31856_TCTYPE_R = 0b0101, // 0x05 + MAX31856_TCTYPE_S = 0b0110, // 0x06 + MAX31856_TCTYPE_T = 0b0111, // 0x07 + MAX31856_VMODE_G8 = 0b1000, // 0x08 + MAX31856_VMODE_G32 = 0b1100, // 0x12 +}; + +enum MAX31856ConfigFilter { + FILTER_60HZ = 0, + FILTER_50HZ = 1, +}; + +class MAX31856Sensor : public sensor::Sensor, + public PollingComponent, + public spi::SPIDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void set_filter(MAX31856ConfigFilter filter) { filter_ = filter; } + void update() override; + + protected: + MAX31856ConfigFilter filter_; + + const uint8_t read_register_(uint8_t reg); + const uint32_t read_register24_(uint8_t reg); + void write_register_(uint8_t reg, uint8_t value); + + void one_shot_temperature_(); + bool has_fault_(); + void clear_fault_(); + void read_thermocouple_temperature_(); + void set_thermocouple_type_(); + void set_noise_filter_(); +}; + +} // namespace max31856 +} // namespace esphome diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py new file mode 100644 index 0000000000..523b5301d4 --- /dev/null +++ b/esphome/components/max31856/sensor.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, spi +from esphome.const import CONF_ID, CONF_MAINS_FILTER, ICON_THERMOMETER, UNIT_CELSIUS + +max31856_ns = cg.esphome_ns.namespace('max31856') +MAX31856Sensor = max31856_ns.class_('MAX31856Sensor', sensor.Sensor, cg.PollingComponent, + spi.SPIDevice) + +MAX31865ConfigFilter = max31856_ns.enum('MAX31856ConfigFilter') +FILTER = { + '50HZ': MAX31865ConfigFilter.FILTER_50HZ, + '60HZ': MAX31865ConfigFilter.FILTER_60HZ, +} + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ + cv.GenerateID(): cv.declare_id(MAX31856Sensor), + cv.Optional(CONF_MAINS_FILTER, default='60HZ'): cv.enum(FILTER, upper=True, space=''), +}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + yield sensor.register_sensor(var, config) + cg.add(var.set_filter(config[CONF_MAINS_FILTER])) diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index a3c537a2c2..d6334a3c34 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -83,7 +83,7 @@ void MAX31865Sensor::dump_config() { LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, " Reference Resistance: %.2fΩ", reference_resistance_); ESP_LOGCONFIG(TAG, " RTD: %u-wire %.2fΩ", rtd_wires_, rtd_nominal_resistance_); - ESP_LOGCONFIG(TAG, " Filter: %s", + ESP_LOGCONFIG(TAG, " Mains Filter: %s", (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); } diff --git a/tests/test1.yaml b/tests/test1.yaml index 2171be844a..b7d6fe7a9d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -498,6 +498,11 @@ sensor: update_interval: 15s reference_temperature: name: "MAX31855 Internal Temperature" + - platform: max31856 + name: "BBQ Temperature" + cs_pin: GPIO17 + update_interval: 15s + mains_filter: 50Hz - platform: max31865 name: "Water Tank Temperature" cs_pin: GPIO23