Skip to content

Instantly share code, notes, and snippets.

@a-a
Created December 6, 2025 00:35
Show Gist options
  • Select an option

  • Save a-a/780fe212a3bf3c5f832dfba2d5b7bf50 to your computer and use it in GitHub Desktop.

Select an option

Save a-a/780fe212a3bf3c5f832dfba2d5b7bf50 to your computer and use it in GitHub Desktop.
Pimoroni MiCS-6814 ESPHome
# 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
#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
#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
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