Feature/add seeed grove gmxxx multichannel gas support (#4304)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
YorkshireIoT 2024-10-07 03:34:46 +01:00 committed by GitHub
parent 81f6750211
commit 03a95ee05f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 245 additions and 0 deletions

View File

@ -162,6 +162,7 @@ esphome/components/gps/* @coogle
esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte
esphome/components/gt911/* @clydebarrow @jesserockz

View File

@ -0,0 +1,88 @@
#include "grove_gas_mc_v2.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace grove_gas_mc_v2 {
static const char *const TAG = "grove_gas_mc_v2";
// I2C Commands for Grove Gas Multichannel V2 Sensor
// Taken from:
// https://github.com/Seeed-Studio/Seeed_Arduino_MultiGas/blob/master/src/Multichannel_Gas_GroveGasMultichannelV2.h
static const uint8_t GROVE_GAS_MC_V2_HEAT_ON = 0xFE;
static const uint8_t GROVE_GAS_MC_V2_HEAT_OFF = 0xFF;
static const uint8_t GROVE_GAS_MC_V2_READ_GM102B = 0x01;
static const uint8_t GROVE_GAS_MC_V2_READ_GM302B = 0x03;
static const uint8_t GROVE_GAS_MC_V2_READ_GM502B = 0x05;
static const uint8_t GROVE_GAS_MC_V2_READ_GM702B = 0x07;
bool GroveGasMultichannelV2Component::read_sensor_(uint8_t address, sensor::Sensor *sensor) {
if (sensor == nullptr) {
return true;
}
uint32_t value = 0;
if (!this->read_bytes(address, (uint8_t *) &value, 4)) {
ESP_LOGW(TAG, "Reading Grove Gas Sensor data failed!");
this->error_code_ = COMMUNICATION_FAILED;
this->status_set_warning();
return false;
}
sensor->publish_state(value);
return true;
}
void GroveGasMultichannelV2Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up Grove Multichannel Gas Sensor V2...");
// Before reading sensor values, must preheat sensor
if (!(this->write_bytes(GROVE_GAS_MC_V2_HEAT_ON, {}))) {
this->mark_failed();
this->error_code_ = APP_START_FAILED;
}
}
void GroveGasMultichannelV2Component::update() {
// Read from each of the gas sensors
if (!this->read_sensor_(GROVE_GAS_MC_V2_READ_GM102B, this->nitrogen_dioxide_sensor_))
return;
if (!this->read_sensor_(GROVE_GAS_MC_V2_READ_GM302B, this->ethanol_sensor_))
return;
if (!this->read_sensor_(GROVE_GAS_MC_V2_READ_GM502B, this->tvoc_sensor_))
return;
if (!this->read_sensor_(GROVE_GAS_MC_V2_READ_GM702B, this->carbon_monoxide_sensor_))
return;
this->status_clear_warning();
}
void GroveGasMultichannelV2Component::dump_config() {
ESP_LOGCONFIG(TAG, "Grove Multichannel Gas Sensor V2");
LOG_I2C_DEVICE(this)
LOG_UPDATE_INTERVAL(this)
LOG_SENSOR(" ", "Nitrogen Dioxide", this->nitrogen_dioxide_sensor_)
LOG_SENSOR(" ", "Ethanol", this->ethanol_sensor_)
LOG_SENSOR(" ", "Carbon Monoxide", this->carbon_monoxide_sensor_)
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_)
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
break;
case APP_INVALID:
ESP_LOGW(TAG, "Sensor reported invalid APP installed.");
break;
case APP_START_FAILED:
ESP_LOGW(TAG, "Sensor reported APP start failed.");
break;
case UNKNOWN:
default:
ESP_LOGW(TAG, "Unknown setup error!");
break;
}
}
}
} // namespace grove_gas_mc_v2
} // namespace esphome

View File

@ -0,0 +1,39 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
namespace esphome {
namespace grove_gas_mc_v2 {
class GroveGasMultichannelV2Component : public PollingComponent, public i2c::I2CDevice {
SUB_SENSOR(tvoc)
SUB_SENSOR(carbon_monoxide)
SUB_SENSOR(nitrogen_dioxide)
SUB_SENSOR(ethanol)
public:
/// Setup the sensor and test for a connection.
void setup() override;
/// Schedule temperature+pressure readings.
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
enum ErrorCode {
UNKNOWN,
COMMUNICATION_FAILED,
APP_INVALID,
APP_START_FAILED,
} error_code_{UNKNOWN};
bool read_sensor_(uint8_t address, sensor::Sensor *sensor);
};
} // namespace grove_gas_mc_v2
} // namespace esphome

View File

@ -0,0 +1,77 @@
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_CARBON_MONOXIDE,
CONF_ETHANOL,
CONF_ID,
CONF_NITROGEN_DIOXIDE,
CONF_TVOC,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_NITROGEN_DIOXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
ICON_AIR_FILTER,
ICON_FLASK_ROUND_BOTTOM,
ICON_GAS_CYLINDER,
ICON_MOLECULE_CO,
STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER,
UNIT_PARTS_PER_MILLION,
)
CODEOWNERS = ["@YorkshireIoT"]
DEPENDENCIES = ["i2c"]
grove_gas_mc_v2_ns = cg.esphome_ns.namespace("grove_gas_mc_v2")
GroveGasMultichannelV2Component = grove_gas_mc_v2_ns.class_(
"GroveGasMultichannelV2Component", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(GroveGasMultichannelV2Component),
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_AIR_FILTER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CARBON_MONOXIDE): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_MONOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_NITROGEN_DIOXIDE): sensor.sensor_schema(
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_GAS_CYLINDER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_NITROGEN_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ETHANOL): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_FLASK_ROUND_BOTTOM,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x08))
)
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 key in [CONF_TVOC, CONF_CARBON_MONOXIDE, CONF_NITROGEN_DIOXIDE, CONF_ETHANOL]:
if sensor_config := config.get(key):
sensor_ = await sensor.new_sensor(sensor_config)
cg.add(getattr(var, f"set_{key}_sensor")(sensor_))

View File

@ -962,6 +962,7 @@ ICON_ACCELERATION_Y = "mdi:axis-y-arrow"
ICON_ACCELERATION_Z = "mdi:axis-z-arrow"
ICON_ACCOUNT = "mdi:account"
ICON_ACCOUNT_CHECK = "mdi:account-check"
ICON_AIR_FILTER = "mdi:air-filter"
ICON_ARROW_EXPAND_VERTICAL = "mdi:arrow-expand-vertical"
ICON_BATTERY = "mdi:battery"
ICON_BLUETOOTH = "mdi:bluetooth"
@ -983,6 +984,7 @@ ICON_FINGERPRINT = "mdi:fingerprint"
ICON_FLASH = "mdi:flash"
ICON_FLASK = "mdi:flask"
ICON_FLASK_OUTLINE = "mdi:flask-outline"
ICON_FLASK_ROUND_BOTTOM = "mdi:flask-round-bottom"
ICON_FLOWER = "mdi:flower"
ICON_GAS_CYLINDER = "mdi:gas-cylinder"
ICON_GAUGE = "mdi:gauge"
@ -995,6 +997,7 @@ ICON_KEY_PLUS = "mdi:key-plus"
ICON_LIGHTBULB = "mdi:lightbulb"
ICON_MAGNET = "mdi:magnet"
ICON_MEMORY = "mdi:memory"
ICON_MOLECULE_CO = "mdi:molecule-co"
ICON_MOLECULE_CO2 = "mdi:molecule-co2"
ICON_MOTION_SENSOR = "mdi:motion-sensor"
ICON_NEW_BOX = "mdi:new-box"

View File

@ -0,0 +1,13 @@
sensor:
- platform: grove_gas_mc_v2
i2c_id: i2c_bus
nitrogen_dioxide:
name: "Nitrogen Dioxide"
ethanol:
name: "Ethanol"
carbon_monoxide:
name: "Carbon Monoxide"
tvoc:
name: "Volatile Organic Compounds"
update_interval: 30s
address: 0xAD

View File

@ -0,0 +1,6 @@
i2c:
sda: 21
scl: 22
id: i2c_bus
<<: !include common.yaml

View File

@ -0,0 +1,6 @@
i2c:
sda: 21
scl: 22
id: i2c_bus
<<: !include common.yaml

View File

@ -0,0 +1,6 @@
i2c:
sda: 4
scl: 5
id: i2c_bus
<<: !include common.yaml

View File

@ -0,0 +1,6 @@
i2c:
sda: 21
scl: 22
id: i2c_bus
<<: !include common.yaml