Created
December 6, 2025 00:35
-
-
Save a-a/780fe212a3bf3c5f832dfba2d5b7bf50 to your computer and use it in GitHub Desktop.
Pimoroni MiCS-6814 ESPHome
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # usage: replace - in filenames with / | |
| # ie: | |
| # sensor.yaml | |
| # local_components/mics_6814/* | |
| # | |
| esphome: | |
| name: gas-sensor-1 | |
| friendly_name: gas-sensor-1 | |
| esp32: | |
| board: esp32-s3-devkitc-1 | |
| framework: | |
| type: esp-idf | |
| # Enable logging | |
| logger: | |
| # Enable Home Assistant API | |
| api: | |
| encryption: | |
| key: !secret encryption_key | |
| ota: | |
| - platform: esphome | |
| password: !secret ota_password | |
| wifi: | |
| ssid: !secret wifi_ssid | |
| password: !secret wifi_password | |
| # Enable fallback hotspot (captive portal) in case wifi connection fails | |
| ap: | |
| ssid: "Gas-Sensor-1 Fallback Hotspot" | |
| password: !secret fallback_password | |
| captive_portal: | |
| external_components: | |
| - source: local_components | |
| i2c: | |
| - sda: GPIO01 | |
| scl: GPIO02 | |
| scan: true | |
| sensor: | |
| - platform: mics_6814 #approx 100ma when heater enabled, else 1-2ma. feed it 5v. 3v3 flakey. | |
| address: 0x19 | |
| carbon_monoxide: | |
| name: "MICS6814 CO (reducing)" | |
| ammonia: | |
| name: "MICS6814 NH3" | |
| nitrogen_dioxide: | |
| name: "MICS6814 NO2 (oxidising)" | |
| update_interval: 60s |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # leave this empty |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include "mics_6814.h" | |
| #include "esphome/core/log.h" | |
| namespace esphome { | |
| namespace mics_6814 { | |
| static const char *const TAG = "mics6814"; | |
| // IO Expander registers | |
| static const uint8_t REG_P1M1 = 0x73; | |
| static const uint8_t REG_P1M2 = 0x74; | |
| static const uint8_t REG_P1 = 0x50; | |
| static const uint8_t REG_ADCRL = 0x82; | |
| static const uint8_t REG_ADCRH = 0x83; | |
| static const uint8_t REG_ADCCON0 = 0xA8; | |
| static const uint8_t REG_ADCCON1 = 0xA1; | |
| static const uint8_t REG_AINDIDS = 0xB6; | |
| static const uint8_t ADCF_BIT = 7; | |
| static const uint8_t ADCS_BIT = 6; | |
| static const float ADC_DENOM = 4095.0f; | |
| // Map IOE pin IDs to ADC channel indices | |
| static uint8_t adc_channel_for_pin(uint8_t pin) { | |
| switch (pin) { | |
| case 14: return 0; // VREF | |
| case 13: return 2; // RED (CO) | |
| case 11: return 3; // NH3 | |
| case 12: return 1; // OX (NO2) | |
| default: return 0xFF; | |
| } | |
| } | |
| inline bool raw_write8(i2c::I2CDevice *dev, uint8_t reg, uint8_t value) { | |
| uint8_t payload[2] = {reg, value}; | |
| return dev->write(payload, 2) == i2c::ERROR_OK; | |
| } | |
| inline bool raw_read(i2c::I2CDevice *dev, uint8_t reg, uint8_t *buf, size_t len) { | |
| uint8_t r = reg; | |
| if (dev->write(&r, 1) != i2c::ERROR_OK) return false; | |
| return dev->read(buf, len) == i2c::ERROR_OK; | |
| } | |
| inline bool raw_read8(i2c::I2CDevice *dev, uint8_t reg, uint8_t &value) { | |
| uint8_t b = 0; | |
| if (!raw_read(dev, reg, &b, 1)) return false; | |
| value = b; | |
| return true; | |
| } | |
| inline bool write_port_bit(i2c::I2CDevice *dev, uint8_t reg_p, uint8_t bit_index, bool state) { | |
| uint8_t opcode = (state ? 0x08 : 0x00) | (bit_index & 0x07); | |
| return raw_write8(dev, reg_p, opcode); | |
| } | |
| inline bool read12(i2c::I2CDevice *dev, uint8_t reg_l, uint8_t reg_h, uint16_t &value) { | |
| uint8_t buf[2] = {0, 0}; | |
| if (!raw_read(dev, reg_l, buf, 2)) return false; | |
| value = static_cast<uint16_t>(buf[0]) | (static_cast<uint16_t>(buf[1]) << 4); | |
| return true; | |
| } | |
| uint16_t read_adc_raw(i2c::I2CDevice *dev, uint8_t pin) { | |
| uint8_t ch = adc_channel_for_pin(pin); | |
| if (ch == 0xFF) return 0; | |
| raw_write8(dev, REG_ADCCON1, 0x01); // enable ADC | |
| raw_write8(dev, REG_AINDIDS, 0x00); // clear selection | |
| raw_write8(dev, REG_AINDIDS, (1 << ch)); // select channel | |
| raw_write8(dev, REG_ADCCON0, (1 << ADCS_BIT)); // start conversion | |
| uint32_t start = millis(); | |
| while (true) { | |
| uint8_t adcon0; | |
| if (!raw_read8(dev, REG_ADCCON0, adcon0)) return 0; | |
| if (adcon0 & (1 << ADCF_BIT)) break; | |
| if (millis() - start > 1000) return 0; | |
| delay(1); | |
| } | |
| uint16_t raw12_val = 0; | |
| if (!read12(dev, REG_ADCRL, REG_ADCRH, raw12_val)) return 0; | |
| return raw12_val; | |
| } | |
| void MICS6814Component::setup() { | |
| ESP_LOGCONFIG(TAG, "Setting up MiCS-6814..."); | |
| uint8_t pm1 = 0, pm2 = 0; | |
| raw_read8(this, REG_P1M1, pm1); | |
| raw_read8(this, REG_P1M2, pm2); | |
| // Push-pull: PxM1=0, PxM2=1 | |
| pm1 &= ~(1 << 5); | |
| pm2 |= (1 << 5); | |
| raw_write8(this, REG_P1M1, pm1); | |
| raw_write8(this, REG_P1M2, pm2); | |
| write_port_bit(this, REG_P1, 5, false); | |
| ESP_LOGI(TAG, "Heater enabled."); | |
| this->warm_until_ = millis() + 180000; | |
| } | |
| void MICS6814Component::update() { | |
| uint16_t raw_vref = read_adc_raw(this, MICS6814_VREF); | |
| float vref = (static_cast<float>(raw_vref) / ADC_DENOM) * 3.3f; | |
| ESP_LOGD(TAG, "VREF raw=%u -> %.3f V", raw_vref, vref); | |
| if (this->vref_sensor_ != nullptr) | |
| this->vref_sensor_->publish_state(vref); | |
| auto read_voltage = [&](uint8_t pin, const char *label) -> float { | |
| uint16_t raw = read_adc_raw(this, pin); | |
| float volts = (static_cast<float>(raw) / ADC_DENOM) * vref; | |
| ESP_LOGD(TAG, "%s raw=%u -> %.3f V", label, raw, volts); | |
| return volts; | |
| }; | |
| float v_red = read_voltage(MICS6814_RED, "CO"); | |
| float v_nh3 = read_voltage(MICS6814_NH3, "NH3"); | |
| float v_oxd = read_voltage(MICS6814_OX, "NO2"); | |
| auto calc_res = [&](float vgas) -> float { | |
| if (vgas <= 0.0f || vgas >= vref) return 0.0f; | |
| return (vgas * 56000.0f) / (vref - vgas); | |
| }; | |
| float r_red = calc_res(v_red); | |
| float r_nh3 = calc_res(v_nh3); | |
| float r_oxd = calc_res(v_oxd); | |
| ESP_LOGD(TAG, "Resistances: CO=%.2fΩ NH3=%.2fΩ NO2=%.2fΩ", r_red, r_nh3, r_oxd); | |
| if (this->warm_until_ > millis()) { | |
| ESP_LOGD(TAG, "Sensor warming, skipping publish_state..."); | |
| return; | |
| } | |
| if (this->carbon_monoxide_sensor_ != nullptr) | |
| this->carbon_monoxide_sensor_->publish_state(r_red); | |
| if (this->ammonia_sensor_ != nullptr) | |
| this->ammonia_sensor_->publish_state(r_nh3); | |
| if (this->nitrogen_dioxide_sensor_ != nullptr) | |
| this->nitrogen_dioxide_sensor_->publish_state(r_oxd); | |
| } | |
| void MICS6814Component::dump_config() { | |
| ESP_LOGCONFIG(TAG, "MiCS-6814 Sensor:"); | |
| sensor::log_sensor(TAG, " ", "Carbon Monoxide (reducing)", this->carbon_monoxide_sensor_); | |
| sensor::log_sensor(TAG, " ", "Ammonia (NH3)", this->ammonia_sensor_); | |
| sensor::log_sensor(TAG, " ", "Nitrogen Dioxide (oxidising)", this->nitrogen_dioxide_sensor_); | |
| } | |
| float MICS6814Component::get_setup_priority() const { | |
| return setup_priority::DATA; | |
| } | |
| } // namespace mics_6814 | |
| } // namespace esphome |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #pragma once | |
| #include "esphome/components/i2c/i2c.h" | |
| #include "esphome/components/sensor/sensor.h" | |
| #include "esphome/core/component.h" | |
| #include "esphome/core/hal.h" | |
| namespace esphome { | |
| namespace mics_6814 { | |
| static const uint8_t MICS6814_I2C_ADDR = 0x19; | |
| static const uint8_t MICS6814_VREF = 14; // ADC ref | |
| static const uint8_t MICS6814_RED = 13; // Reducing gases (carbon monoxide) | |
| static const uint8_t MICS6814_NH3 = 11; // Ammonia | |
| static const uint8_t MICS6814_OX = 12; // Oxidising gases (nitrogen dioxide) | |
| static const uint8_t MICS6814_HEATER_EN = 1; | |
| class MICS6814Component : public PollingComponent, public i2c::I2CDevice { | |
| SUB_SENSOR(carbon_monoxide) | |
| SUB_SENSOR(ammonia) | |
| SUB_SENSOR(nitrogen_dioxide) | |
| SUB_SENSOR(vref) | |
| public: | |
| void setup() override; | |
| void dump_config() override; | |
| float get_setup_priority() const override; | |
| void update() override; | |
| protected: | |
| bool warmed_up_{false}; | |
| bool initial_{true}; | |
| float ox_calibration_{0}; | |
| float red_calibration_{0}; | |
| uint32_t warm_until_{0}; | |
| }; | |
| } // namespace mics_6814 | |
| } // namespace esphome |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import esphome.codegen as cg | |
| from esphome.components import i2c, sensor | |
| import esphome.config_validation as cv | |
| from esphome.const import ( | |
| CONF_AMMONIA, | |
| CONF_CARBON_MONOXIDE, | |
| CONF_NITROGEN_DIOXIDE, | |
| CONF_ID, | |
| DEVICE_CLASS_CARBON_MONOXIDE, | |
| DEVICE_CLASS_EMPTY, | |
| DEVICE_CLASS_VOLTAGE, | |
| STATE_CLASS_MEASUREMENT, | |
| UNIT_OHM, | |
| UNIT_VOLT, | |
| ) | |
| CODEOWNERS = ["nobody"] | |
| DEPENDENCIES = ["i2c"] | |
| mics_6814_ns = cg.esphome_ns.namespace("mics_6814") | |
| MICS6814Component = mics_6814_ns.class_( | |
| "MICS6814Component", cg.PollingComponent, i2c.I2CDevice | |
| ) | |
| CONF_VREF = "vref" | |
| SENSORS = { | |
| CONF_CARBON_MONOXIDE: (DEVICE_CLASS_CARBON_MONOXIDE, UNIT_OHM, 2), | |
| CONF_AMMONIA: (DEVICE_CLASS_EMPTY, UNIT_OHM, 2), | |
| CONF_NITROGEN_DIOXIDE: (DEVICE_CLASS_EMPTY, UNIT_OHM, 2), | |
| CONF_VREF: (DEVICE_CLASS_VOLTAGE, UNIT_VOLT, 3), | |
| } | |
| def common_sensor_schema(*, device_class: str, unit: str, decimals: int) -> cv.Schema: | |
| return sensor.sensor_schema( | |
| accuracy_decimals=decimals, | |
| device_class=device_class, | |
| state_class=STATE_CLASS_MEASUREMENT, | |
| unit_of_measurement=unit, | |
| ) | |
| CONFIG_SCHEMA = ( | |
| cv.Schema({cv.GenerateID(): cv.declare_id(MICS6814Component)}) | |
| .extend( | |
| { | |
| cv.Optional(sensor_type): common_sensor_schema( | |
| device_class=device_class, unit=unit, decimals=decimals | |
| ) | |
| for sensor_type, (device_class, unit, decimals) in SENSORS.items() | |
| } | |
| ) | |
| .extend(i2c.i2c_device_schema(0x19)) | |
| .extend(cv.polling_component_schema("60s")) | |
| ) | |
| 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) | |
| for sensor_type in SENSORS: | |
| if sensor_config := config.get(sensor_type): | |
| sens = await sensor.new_sensor(sensor_config) | |
| cg.add(getattr(var, f"set_{sensor_type}_sensor")(sens)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment