diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 0462ed4342..88f9e836f9 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -1,10 +1,11 @@ #include "max31855.h" + #include "esphome/core/log.h" namespace esphome { namespace max31855 { -static const char *TAG = "max31855"; +static const char* TAG = "max31855"; void MAX31855Sensor::update() { this->enable(); @@ -22,9 +23,15 @@ void MAX31855Sensor::setup() { this->spi_setup(); } void MAX31855Sensor::dump_config() { - LOG_SENSOR("", "MAX31855", this); + ESP_LOGCONFIG(TAG, "MAX31855:"); LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Thermocouple", this); + if (this->temperature_reference_) { + LOG_SENSOR(" ", "Reference", this->temperature_reference_); + } else { + ESP_LOGCONFIG(TAG, " Reference temperature disabled."); + } } float MAX31855Sensor::get_setup_priority() const { return setup_priority::DATA; } void MAX31855Sensor::read_data_() { @@ -32,53 +39,68 @@ void MAX31855Sensor::read_data_() { delay(1); uint8_t data[4]; this->read_array(data, 4); - - // val is 14 bits of signed temperature data followed by 2 bits of status flags - int16_t val = data[1] | data[0] << 8; - - // test data from MAX31855 datasheet - // val = 0x6400 // 1600.00°C - // val = 0x3E80 // 1000.00°C - // val = 0x064C // 100.75°C - // val = 0x0190 // 25.00°C - // val = 0x0000 // 0.00°C - // val = 0xFFFC // -0.25°C - // val = 0xFFF0 // -1.00°C - // val = 0xF060 // -250.00°C - this->disable(); - if ((data[3] & 0x01) != 0) { - ESP_LOGW(TAG, "Got thermocouple not connected from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((data[3] & 0x02) != 0) { - ESP_LOGW(TAG, "Got short circuit to ground from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((data[3] & 0x04) != 0) { - ESP_LOGW(TAG, "Got short circuit to power from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((data[1] & 0x01) != 0) { - ESP_LOGW(TAG, "Got faulty reading from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((val & 0x8000) != 0) { - // Negative value, drop the lower 2 bits and explicitly extend sign bits. - val = 0xE000 | ((val >> 2) & 0x1FFF); + const uint32_t mem = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3] << 0; + + // Verify we got data + if (mem != 0 && mem != 0xFFFFFFFF) { + this->status_clear_error(); } else { - // Positive value, just drop the lower 2 bits. - val >>= 2; + ESP_LOGE(TAG, "No data received from MAX31855 (0x%08X). Check wiring!", mem); + this->publish_state(NAN); + if (this->temperature_reference_) { + this->temperature_reference_->publish_state(NAN); + } + this->status_set_error(); + return; } - float temperature = float(val) / 4.0f; - ESP_LOGD(TAG, "'%s': Got temperature=%.1f°C", this->name_.c_str(), temperature); - this->publish_state(temperature); + // Internal reference temperature always works + if (this->temperature_reference_) { + int16_t val = (mem & 0x0000FFF0) >> 4; + if (val & 0x0800) { + val |= 0xF000; // Pad out 2's complement + } + const float t_ref = float(val) * 0.0625f; + ESP_LOGD(TAG, "Got reference temperature: %.4f°C", t_ref); + this->temperature_reference_->publish_state(t_ref); + } + + // Check thermocouple faults + if (mem & 0x00000001) { + ESP_LOGW(TAG, "Thermocouple open circuit (not connected) fault from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + if (mem & 0x00000002) { + ESP_LOGW(TAG, "Thermocouple short circuit to ground fault from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + if (mem & 0x00000004) { + ESP_LOGW(TAG, "Thermocouple short circuit to VCC fault from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + if (mem & 0x00010000) { + ESP_LOGW(TAG, "Got faulty reading from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + + // Decode thermocouple temperature + int16_t val = (mem & 0xFFFC0000) >> 18; + if (val & 0x2000) { + val |= 0xC000; // Pad out 2's complement + } + const float t_sense = float(val) * 0.25f; + ESP_LOGD(TAG, "Got thermocouple temperature: %.2f°C", t_sense); + this->publish_state(t_sense); this->status_clear_warning(); } diff --git a/esphome/components/max31855/max31855.h b/esphome/components/max31855/max31855.h index 1d0fc79ac0..c0ed8a467d 100644 --- a/esphome/components/max31855/max31855.h +++ b/esphome/components/max31855/max31855.h @@ -9,9 +9,11 @@ namespace max31855 { class MAX31855Sensor : public sensor::Sensor, public PollingComponent, - public spi::SPIDevice { + public spi::SPIDevice { public: + void set_reference_sensor(sensor::Sensor *temperature_sensor) { temperature_reference_ = temperature_sensor; } + void setup() override; void dump_config() override; float get_setup_priority() const override; @@ -20,6 +22,7 @@ class MAX31855Sensor : public sensor::Sensor, protected: void read_data_(); + sensor::Sensor *temperature_reference_{nullptr}; }; } // namespace max31855 diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index b4d9f82b03..dce28bd542 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi -from esphome.const import CONF_ID, ICON_THERMOMETER, UNIT_CELSIUS +from esphome.const import CONF_ID, CONF_REFERENCE_TEMPERATURE, ICON_THERMOMETER, UNIT_CELSIUS max31855_ns = cg.esphome_ns.namespace('max31855') MAX31855Sensor = max31855_ns.class_('MAX31855Sensor', sensor.Sensor, cg.PollingComponent, @@ -9,6 +9,8 @@ MAX31855Sensor = max31855_ns.class_('MAX31855Sensor', sensor.Sensor, cg.PollingC CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ cv.GenerateID(): cv.declare_id(MAX31855Sensor), + cv.Optional(CONF_REFERENCE_TEMPERATURE): + sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2), }).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) @@ -17,3 +19,6 @@ def to_code(config): yield cg.register_component(var, config) yield spi.register_spi_device(var, config) yield sensor.register_sensor(var, config) + if CONF_REFERENCE_TEMPERATURE in config: + tc_ref = yield sensor.new_sensor(config[CONF_REFERENCE_TEMPERATURE]) + cg.add(var.set_reference_sensor(tc_ref)) diff --git a/esphome/components/max31865/__init__.py b/esphome/components/max31865/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp new file mode 100644 index 0000000000..500b5b2883 --- /dev/null +++ b/esphome/components/max31865/max31865.cpp @@ -0,0 +1,214 @@ +#include "max31865.h" + +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace max31865 { + +static const char* TAG = "max31865"; + +void MAX31865Sensor::update() { + // Check new faults since last measurement + if (!has_fault_) { + const uint8_t faults = this->read_register_(FAULT_STATUS_REG); + if (faults & 0b11111100) { + if (faults & (1 << 2)) { + ESP_LOGW(TAG, "Overvoltage/undervoltage fault between measurements"); + } + if (faults & (1 << 3)) { + ESP_LOGW(TAG, "RTDIN- < 0.85 x V_BIAS (FORCE- open) between measurements"); + } + if (faults & (1 << 4)) { + ESP_LOGW(TAG, "REFIN- < 0.85 x V_BIAS (FORCE- open) between measurements"); + } + if (faults & (1 << 5)) { + ESP_LOGW(TAG, "REFIN- > 0.85 x V_BIAS between measurements"); + } + if (!has_warn_) { + if (faults & (1 << 6)) { + ESP_LOGW(TAG, "RTD Low Threshold between measurements"); + } + if (faults & (1 << 7)) { + ESP_LOGW(TAG, "RTD High Threshold between measurements"); + } + } + } + } + + // Run fault detection + write_register_(CONFIGURATION_REG, 0b11101110, 0b10000110); + const uint32_t start_time = micros(); + uint8_t config; + uint32_t fault_detect_time; + do { + config = this->read_register_(CONFIGURATION_REG); + fault_detect_time = micros() - start_time; + if ((fault_detect_time >= 6000) && (config & 0b00001100)) { + ESP_LOGE(TAG, "Fault detection incomplete (0x%02X) after %uμs (datasheet spec is 600μs max)! Aborting read.", + config, fault_detect_time); + this->publish_state(NAN); + this->status_set_error(); + return; + } + } while (config & 0b00001100); + ESP_LOGV(TAG, "Fault detection completed in %uμs.", fault_detect_time); + + // Start 1-shot conversion + this->write_register_(CONFIGURATION_REG, 0b11100000, 0b10100000); + + // Datasheet max conversion time is 55ms for 60Hz / 66ms for 50Hz + auto f = std::bind(&MAX31865Sensor::read_data_, this); + this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, f); +} + +void MAX31865Sensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX31865Sensor '%s'...", this->name_.c_str()); + this->spi_setup(); + + // Build configuration + uint8_t config = 0b00000010; + config |= (filter_ & 1) << 0; + if (rtd_wires_ == 3) { + config |= 1 << 4; + } + this->write_register_(CONFIGURATION_REG, 0b11111111, config); +} + +void MAX31865Sensor::dump_config() { + LOG_SENSOR("", "MAX31865", this); + LOG_PIN(" CS Pin: ", this->cs_); + 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", + (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); +} + +float MAX31865Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void MAX31865Sensor::read_data_() { + // Read temperature, disable V_BIAS (save power) + const uint16_t rtd_resistance_register = this->read_register_16_(RTD_RESISTANCE_MSB_REG); + this->write_register_(CONFIGURATION_REG, 0b11000000, 0b00000000); + + // Check faults + const uint8_t faults = this->read_register_(FAULT_STATUS_REG); + if ((has_fault_ = faults & 0b00111100)) { + if (faults & (1 << 2)) { + ESP_LOGE(TAG, "Overvoltage/undervoltage fault"); + } + if (faults & (1 << 3)) { + ESP_LOGE(TAG, "RTDIN- < 0.85 x V_BIAS (FORCE- open)"); + } + if (faults & (1 << 4)) { + ESP_LOGE(TAG, "REFIN- < 0.85 x V_BIAS (FORCE- open)"); + } + if (faults & (1 << 5)) { + ESP_LOGE(TAG, "REFIN- > 0.85 x V_BIAS"); + } + this->publish_state(NAN); + this->status_set_error(); + return; + } else { + this->status_clear_error(); + } + if ((has_warn_ = faults & 0b11000000)) { + if (faults & (1 << 6)) { + ESP_LOGW(TAG, "RTD Low Threshold"); + } + if (faults & (1 << 7)) { + ESP_LOGW(TAG, "RTD High Threshold"); + } + this->status_set_warning(); + } else { + this->status_clear_warning(); + } + + // Process temperature + if (rtd_resistance_register & 0x0001) { + ESP_LOGW(TAG, "RTD Resistance Registers fault bit set! (0x%04X)", rtd_resistance_register); + this->status_set_warning(); + } + const float rtd_ratio = static_cast(rtd_resistance_register >> 1) / static_cast((1 << 15) - 1); + const float temperature = this->calc_temperature_(rtd_ratio); + ESP_LOGV(TAG, "RTD read complete. %.5f (ratio) * %.1fΩ (reference) = %.2fΩ --> %.2f°C", rtd_ratio, + reference_resistance_, reference_resistance_ * rtd_ratio, temperature); + this->publish_state(temperature); +} + +void MAX31865Sensor::write_register_(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) { + uint8_t value = this->read_register_(reg); + + value &= (~mask); + value |= (bits << start_position); + + this->enable(); + this->write_byte(reg |= SPI_WRITE_M); + this->write_byte(value); + this->disable(); + ESP_LOGVV(TAG, "write_register_ 0x%02X: 0x%02X", reg, value); +} + +const uint8_t MAX31865Sensor::read_register_(uint8_t reg) { + this->enable(); + this->write_byte(reg); + const uint8_t value(this->read_byte()); + this->disable(); + ESP_LOGVV(TAG, "read_register_ 0x%02X: 0x%02X", reg, value); + return value; +} + +const uint16_t MAX31865Sensor::read_register_16_(uint8_t reg) { + this->enable(); + this->write_byte(reg); + const uint8_t msb(this->read_byte()); + const uint8_t lsb(this->read_byte()); + this->disable(); + const uint16_t value((msb << 8) | lsb); + ESP_LOGVV(TAG, "read_register_16_ 0x%02X: 0x%04X", reg, value); + return value; +} + +float MAX31865Sensor::calc_temperature_(const float& rtd_ratio) { + // Based loosely on Adafruit's library: https://github.com/adafruit/Adafruit_MAX31865 + // Mainly based on formulas provided by Analog: + // http://www.analog.com/media/en/technical-documentation/application-notes/AN709_0.pdf + + const float a = 3.9083e-3; + const float b = -5.775e-7; + const float z1 = -a; + const float z2 = a * a - 4 * b; + const float z3 = 4 * b / rtd_nominal_resistance_; + const float z4 = 2 * b; + + float rtd_resistance = rtd_ratio * reference_resistance_; + + // ≥ 0°C Formula + const float pos_temp = (z1 + std::sqrt(z2 + (z3 * rtd_resistance))) / z4; + if (pos_temp >= 0) { + return pos_temp; + } + + // < 0°C Formula + if (rtd_nominal_resistance_ != 100) { + // Normalize RTD to 100Ω + rtd_resistance /= rtd_nominal_resistance_; + rtd_resistance *= 100; + } + float rpoly = rtd_resistance; + float neg_temp = -242.02; + neg_temp += 2.2228 * rpoly; + rpoly *= rtd_resistance; // square + neg_temp += 2.5859e-3 * rpoly; + rpoly *= rtd_resistance; // ^3 + neg_temp -= 4.8260e-6 * rpoly; + rpoly *= rtd_resistance; // ^4 + neg_temp -= 2.8183e-8 * rpoly; + rpoly *= rtd_resistance; // ^5 + neg_temp += 1.5243e-10 * rpoly; + return neg_temp; +} + +} // namespace max31865 +} // namespace esphome diff --git a/esphome/components/max31865/max31865.h b/esphome/components/max31865/max31865.h new file mode 100644 index 0000000000..e63be8e6c5 --- /dev/null +++ b/esphome/components/max31865/max31865.h @@ -0,0 +1,56 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace max31865 { + +enum MAX31865RegisterMasks { SPI_WRITE_M = 0x80 }; +enum MAX31865Registers { + CONFIGURATION_REG = 0x00, + RTD_RESISTANCE_MSB_REG = 0x01, + RTD_RESISTANCE_LSB_REG = 0x02, + FAULT_THRESHOLD_H_MSB_REG = 0x03, + FAULT_THRESHOLD_H_LSB_REG = 0x04, + FAULT_THRESHOLD_L_MSB_REG = 0x05, + FAULT_THRESHOLD_L_LSB_REG = 0x06, + FAULT_STATUS_REG = 0x07, +}; +enum MAX31865ConfigFilter { + FILTER_60HZ = 0, + FILTER_50HZ = 1, +}; + +class MAX31865Sensor : public sensor::Sensor, + public PollingComponent, + public spi::SPIDevice { + public: + void set_reference_resistance(float reference_resistance) { reference_resistance_ = reference_resistance; } + void set_nominal_resistance(float nominal_resistance) { rtd_nominal_resistance_ = nominal_resistance; } + void set_filter(MAX31865ConfigFilter filter) { filter_ = filter; } + void set_num_rtd_wires(uint8_t rtd_wires) { rtd_wires_ = rtd_wires; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + void update() override; + + protected: + float reference_resistance_; + float rtd_nominal_resistance_; + MAX31865ConfigFilter filter_; + uint8_t rtd_wires_; + bool has_fault_ = false; + bool has_warn_ = false; + void read_data_(); + void write_register_(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position = 0); + const uint8_t read_register_(uint8_t reg); + const uint16_t read_register_16_(uint8_t reg); + float calc_temperature_(const float& rtd_ratio); +}; + +} // namespace max31865 +} // namespace esphome diff --git a/esphome/components/max31865/sensor.py b/esphome/components/max31865/sensor.py new file mode 100644 index 0000000000..ff1df9c5c8 --- /dev/null +++ b/esphome/components/max31865/sensor.py @@ -0,0 +1,34 @@ +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, CONF_REFERENCE_RESISTANCE, \ + CONF_RTD_NOMINAL_RESISTANCE, CONF_RTD_WIRES, ICON_THERMOMETER, UNIT_CELSIUS + +max31865_ns = cg.esphome_ns.namespace('max31865') +MAX31865Sensor = max31865_ns.class_('MAX31865Sensor', sensor.Sensor, cg.PollingComponent, + spi.SPIDevice) + +MAX31865ConfigFilter = max31865_ns.enum('MAX31865ConfigFilter') +FILTER = { + '50HZ': MAX31865ConfigFilter.FILTER_50HZ, + '60HZ': MAX31865ConfigFilter.FILTER_60HZ, +} + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2).extend({ + cv.GenerateID(): cv.declare_id(MAX31865Sensor), + cv.Required(CONF_REFERENCE_RESISTANCE): cv.All(cv.resistance, cv.Range(min=100, max=10000)), + cv.Required(CONF_RTD_NOMINAL_RESISTANCE): cv.All(cv.resistance, cv.Range(min=100, max=1000)), + cv.Optional(CONF_MAINS_FILTER, default='60HZ'): cv.enum(FILTER, upper=True, space=''), + cv.Optional(CONF_RTD_WIRES, default=4): cv.int_range(min=2, max=4), +}).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_reference_resistance(config[CONF_REFERENCE_RESISTANCE])) + cg.add(var.set_nominal_resistance(config[CONF_RTD_NOMINAL_RESISTANCE])) + cg.add(var.set_filter(config[CONF_MAINS_FILTER])) + cg.add(var.set_num_rtd_wires(config[CONF_RTD_WIRES])) diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index a528183ac8..7ff4f4e137 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -4,15 +4,14 @@ from math import log import esphome.config_validation as cv import esphome.codegen as cg from esphome.components import sensor -from esphome.const import UNIT_CELSIUS, ICON_THERMOMETER, CONF_SENSOR, CONF_TEMPERATURE, \ - CONF_VALUE, CONF_CALIBRATION, CONF_ID +from esphome.const import CONF_CALIBRATION, CONF_ID, CONF_REFERENCE_RESISTANCE, \ + CONF_REFERENCE_TEMPERATURE, CONF_SENSOR, CONF_TEMPERATURE, CONF_VALUE, ICON_THERMOMETER, \ + UNIT_CELSIUS ntc_ns = cg.esphome_ns.namespace('ntc') NTC = ntc_ns.class_('NTC', cg.Component, sensor.Sensor) CONF_B_CONSTANT = 'b_constant' -CONF_REFERENCE_TEMPERATURE = 'reference_temperature' -CONF_REFERENCE_RESISTANCE = 'reference_resistance' CONF_A = 'a' CONF_B = 'b' CONF_C = 'c' diff --git a/esphome/const.py b/esphome/const.py index dc607d62ff..cde9134eae 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -224,6 +224,7 @@ CONF_LOGS = 'logs' CONF_LOW = 'low' CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference' CONF_MAC_ADDRESS = 'mac_address' +CONF_MAINS_FILTER = 'mains_filter' CONF_MAKE_ID = 'make_id' CONF_MANUAL_IP = 'manual_ip' CONF_MASK_DISTURBER = 'mask_disturber' @@ -343,6 +344,8 @@ CONF_RAW = 'raw' CONF_REBOOT_TIMEOUT = 'reboot_timeout' CONF_RECEIVE_TIMEOUT = 'receive_timeout' CONF_RED = 'red' +CONF_REFERENCE_RESISTANCE = 'reference_resistance' +CONF_REFERENCE_TEMPERATURE = 'reference_temperature' CONF_REPEAT = 'repeat' CONF_REPOSITORY = 'repository' CONF_RESET_PIN = 'reset_pin' @@ -358,6 +361,8 @@ CONF_RGBW = 'rgbw' CONF_RISING_EDGE = 'rising_edge' CONF_ROTATION = 'rotation' CONF_RS_PIN = 'rs_pin' +CONF_RTD_NOMINAL_RESISTANCE = 'rtd_nominal_resistance' +CONF_RTD_WIRES = 'rtd_wires' CONF_RUN_CYCLES = 'run_cycles' CONF_RUN_DURATION = 'run_duration' CONF_RW_PIN = 'rw_pin' diff --git a/tests/test1.yaml b/tests/test1.yaml index 66f0220836..60f3b7f418 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -451,6 +451,14 @@ sensor: name: "Den Temperature" cs_pin: GPIO23 update_interval: 15s + reference_temperature: + name: "MAX31855 Internal Temperature" + - platform: max31865 + name: "Water Tank Temperature" + cs_pin: GPIO23 + update_interval: 15s + reference_resistance: "430 Ω" + rtd_nominal_resistance: "100 Ω" - platform: mhz19 co2: name: "MH-Z19 CO2 Value"