diff --git a/CODEOWNERS b/CODEOWNERS index 9f770d4efc..02033d054a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -11,6 +11,8 @@ esphome/*.py @esphome/core esphome/core/* @esphome/core # Integrations +esphome/components/MCP3428/* @mdop +esphome/components/MCP3428/sensor/* @mdop esphome/components/a01nyub/* @MrSuicideParrot esphome/components/a02yyuw/* @TH-Braemer esphome/components/absolute_humidity/* @DAVe3283 diff --git a/esphome/components/MCP3428/__init__.py b/esphome/components/MCP3428/__init__.py new file mode 100644 index 0000000000..0884db0460 --- /dev/null +++ b/esphome/components/MCP3428/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@mdop"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +mcp3428_ns = cg.esphome_ns.namespace("mcp3428") +MCP3428Component = mcp3428_ns.class_("MCP3428Component", cg.Component, i2c.I2CDevice) + +CONF_CONTINUOUS_MODE = "continuous_mode" +CONF_MCP3428_ID = "mcp3428_id" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MCP3428Component), + cv.Optional(CONF_CONTINUOUS_MODE, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(None)) +) + + +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) + + cg.add(var.set_continuous_mode(config[CONF_CONTINUOUS_MODE])) diff --git a/esphome/components/MCP3428/mcp3428.cpp b/esphome/components/MCP3428/mcp3428.cpp new file mode 100644 index 0000000000..c875836592 --- /dev/null +++ b/esphome/components/MCP3428/mcp3428.cpp @@ -0,0 +1,141 @@ +#include "mcp3428.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3428 { + +static const char *const TAG = "mcp3426/7/8"; + +void MCP3428Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP3426/7/8..."); + uint8_t anwser[3]; + if (!this->read(anwser, 3)) { + this->mark_failed(); + return; + } + + ESP_LOGCONFIG(TAG, "Configuring MCP3426/7/8..."); + + /* config byte structure: (bit|description) + * 7 | ready bit, 0 means new data in the result + * 6 | Channel selection bit 1 + * 5 | Channel selection bit 0 + * 4 | conversion mode bit (1 continuous mode, 0 singe shot) + * 3 | Resolution bit 1 + * 2 | Resolution bit 0 + * 1 | Gain selection bit 1 + * 0 | Gain selection bit 0 + */ + uint8_t config = 0; + // set ready bit and conversion mode bit + // 0b0xx0xxxx + if (this->continuous_mode_) { + // initial state should be no new measurement in continuous mode + config = config | 0b10010000; + } else { + // don't initiate measurement in single shot mode + config = config & 0b01101111; + } + // leave channel at 1, gain at 1x, and resolution at 12 bit + + if (!this->write(&config, 1)) { + this->mark_failed(); + return; + } + this->prev_config_ = config; +} + +void MCP3428Component::dump_config() { + ESP_LOGCONFIG(TAG, "Setting up MCP3426/7/8..."); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with MCP3426/7/8 failed!"); + } +} + +float MCP3428Component::request_measurement(MCP3428Multiplexer multiplexer, MCP3428Gain gain, + MCP3428Resolution resolution) { + uint8_t config = 0; + // set ready bit to 1, will starts measurement in single shot mode and mark measurement as not yet ready in continuous + // mode + config |= 1 << 7; + // set channel + config |= multiplexer << 5; + // set conversion mode + if (this->continuous_mode_) { + config |= 1 << 4; + } + // set resolution + config |= resolution << 2; + // set gain + config |= gain; + + // If continuous mode and config (besides ready bit) are the same there is no need to upload new config, reading the + // result is enough + if (!((this->prev_config_ & 0b00010000) > 0 and (this->prev_config_ & 0b01111111) == (config & 0b01111111))) { + if (!this->write(&config, 1)) { + this->status_set_warning(); + return NAN; + } + this->prev_config_ = config; + } + + // MCP is now configured, read output until ready flag is 0 for a valid measurement + uint32_t start = millis(); + uint8_t anwser[3]; + while (true) { + if (!this->read(anwser, 3)) { + this->status_set_warning(); + return NAN; + } + if ((anwser[2] & 0b10000000) == 0) { + // ready flag is 0, valid measurement received + break; + } + if (millis() - start > 100) { + ESP_LOGW(TAG, "Reading MCP3428 measurement timed out"); + this->status_set_warning(); + return NAN; + } + yield(); + } + + // got valid measurement, clean up unused bits from the anwser code and prepare tick size + float tick_voltage = 2.048f / 32768; // ref voltage 2.048V/non-sign bits, default 15 bits + switch (resolution) { + case MCP3428Resolution::MCP3428_12_BITS: + // Structure [sign][sign][sign][sign][sign][value][value][value], only keep sign at MSB for int16_t + anwser[0] &= 0b10000111; + tick_voltage *= 16; + break; + case MCP3428Resolution::MCP3428_14_BITS: + // Structure [sign][sign][sign][value][value][value][value][value] + anwser[0] &= 0b10011111; + tick_voltage *= 4; + break; + default: // nothing to do for 16 bit + break; + } + switch (gain) { + case MCP3428Gain::MCP3428_GAIN_2: + tick_voltage /= 2; + break; + case MCP3428Gain::MCP3428_GAIN_4: + tick_voltage /= 4; + break; + case MCP3428Gain::MCP3428_GAIN_8: + tick_voltage /= 8; + break; + default: + break; + } + // convert code (first 2 bytes of cleaned up anwser) into voltage ticks + int16_t ticks = anwser[0] << 8 | anwser[1]; + + this->status_clear_warning(); + return tick_voltage * ticks; +} + +} // namespace mcp3428 +} // namespace esphome diff --git a/esphome/components/MCP3428/mcp3428.h b/esphome/components/MCP3428/mcp3428.h new file mode 100644 index 0000000000..177615b1d5 --- /dev/null +++ b/esphome/components/MCP3428/mcp3428.h @@ -0,0 +1,47 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace mcp3428 { + +// the second bit is ignored in MCP3426/7 and will result in measurement of channel 1 or 2 +enum MCP3428Multiplexer { + MCP3428_MULTIPLEXER_CHANNEL_1 = 0b00, + MCP3428_MULTIPLEXER_CHANNEL_2 = 0b01, + MCP3428_MULTIPLEXER_CHANNEL_3 = 0b10, + MCP3428_MULTIPLEXER_CHANNEL_4 = 0b11, +}; + +enum MCP3428Gain { + MCP3428_GAIN_1 = 0b00, + MCP3428_GAIN_2 = 0b01, + MCP3428_GAIN_4 = 0b10, + MCP3428_GAIN_8 = 0b11, +}; + +enum MCP3428Resolution { + MCP3428_12_BITS = 0b00, + MCP3428_14_BITS = 0b01, + MCP3428_16_BITS = 0b10, +}; + +class MCP3428Component : public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + /// HARDWARE_LATE setup priority + float get_setup_priority() const override { return setup_priority::DATA; } + void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } + + /// Helper method to request a measurement from a sensor. + float request_measurement(MCP3428Multiplexer multiplexer, MCP3428Gain gain, MCP3428Resolution resolution); + + protected: + uint8_t prev_config_{0}; + bool continuous_mode_; +}; + +} // namespace mcp3428 +} // namespace esphome diff --git a/esphome/components/MCP3428/sensor/__init__.py b/esphome/components/MCP3428/sensor/__init__.py new file mode 100644 index 0000000000..f61f97f1ba --- /dev/null +++ b/esphome/components/MCP3428/sensor/__init__.py @@ -0,0 +1,75 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import ( + CONF_GAIN, + CONF_MULTIPLEXER, + CONF_RESOLUTION, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, + CONF_ID, +) +from .. import mcp3428_ns, MCP3428Component, CONF_MCP3428_ID + +CODEOWNERS = ["@mdop"] +AUTO_LOAD = ["voltage_sampler"] +DEPENDENCIES = ["mcp3428"] + +MCP3428Multiplexer = mcp3428_ns.enum("MCP3428Multiplexer") +MUX = { + 1: MCP3428Multiplexer.MCP3428_MULTIPLEXER_CHANNEL_1, + 2: MCP3428Multiplexer.MCP3428_MULTIPLEXER_CHANNEL_2, + 3: MCP3428Multiplexer.MCP3428_MULTIPLEXER_CHANNEL_3, + 4: MCP3428Multiplexer.MCP3428_MULTIPLEXER_CHANNEL_4, +} + +MCP3428Gain = mcp3428_ns.enum("MCP3428Gain") +GAIN = { + 1: MCP3428Gain.MCP3428_GAIN_1, + 2: MCP3428Gain.MCP3428_GAIN_2, + 4: MCP3428Gain.MCP3428_GAIN_4, + 8: MCP3428Gain.MCP3428_GAIN_8, +} + +MCP3428Resolution = mcp3428_ns.enum("MCP3428Resolution") +RESOLUTION = { + 12: MCP3428Resolution.MCP3428_12_BITS, + 14: MCP3428Resolution.MCP3428_14_BITS, + 16: MCP3428Resolution.MCP3428_16_BITS, +} + + +MCP3428Sensor = mcp3428_ns.class_( + "MCP3428Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + MCP3428Sensor, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=6, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_MCP3428_ID): cv.use_id(MCP3428Component), + cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, int=True), + cv.Optional(CONF_GAIN, default=1): cv.enum(GAIN, int=True), + cv.Optional(CONF_RESOLUTION, default=16): cv.enum(RESOLUTION, int=True), + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await sensor.register_sensor(var, config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_MCP3428_ID]) + + cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) + cg.add(var.set_gain(config[CONF_GAIN])) + cg.add(var.set_resolution(config[CONF_RESOLUTION])) diff --git a/esphome/components/MCP3428/sensor/mcp3428_sensor.cpp b/esphome/components/MCP3428/sensor/mcp3428_sensor.cpp new file mode 100644 index 0000000000..221fb5f2e1 --- /dev/null +++ b/esphome/components/MCP3428/sensor/mcp3428_sensor.cpp @@ -0,0 +1,30 @@ +#include "mcp3428_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3428 { + +static const char *const TAG = "mcp3426/7/8.sensor"; + +float MCP3428Sensor::sample() { + return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_); +} + +void MCP3428Sensor::update() { + float v = this->sample(); + if (!std::isnan(v)) { + ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v); + this->publish_state(v); + } +} + +void MCP3428Sensor::dump_config() { + LOG_SENSOR(" ", "MCP3426/7/8 Sensor", this); + ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_); + ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_); + ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_); +} + +} // namespace mcp3428 +} // namespace esphome diff --git a/esphome/components/MCP3428/sensor/mcp3428_sensor.h b/esphome/components/MCP3428/sensor/mcp3428_sensor.h new file mode 100644 index 0000000000..2c7d45a2ee --- /dev/null +++ b/esphome/components/MCP3428/sensor/mcp3428_sensor.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +#include "../mcp3428.h" + +namespace esphome { +namespace mcp3428 { + +/// Internal holder class that is in instance of Sensor so that the hub can create individual sensors. +class MCP3428Sensor : public sensor::Sensor, + public PollingComponent, + public voltage_sampler::VoltageSampler, + public Parented { + public: + void update() override; + void set_multiplexer(MCP3428Multiplexer multiplexer) { this->multiplexer_ = multiplexer; } + void set_gain(MCP3428Gain gain) { this->gain_ = gain; } + void set_resolution(MCP3428Resolution resolution) { this->resolution_ = resolution; } + float sample() override; + + void dump_config() override; + + protected: + MCP3428Multiplexer multiplexer_; + MCP3428Gain gain_; + MCP3428Resolution resolution_; +}; + +} // namespace mcp3428 +} // namespace esphome diff --git a/tests/components/MCP3428/test.esp32-c3-idf.yaml b/tests/components/MCP3428/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a1ba9f0129 --- /dev/null +++ b/tests/components/MCP3428/test.esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp3428 + scl: 16 + sda: 17 + +mcp3428: + address: 0b1101000 + +sensor: + - platform: mcp3428 + multiplexer: 1 + id: mcp3428_sensor diff --git a/tests/components/MCP3428/test.esp32-c3.yaml b/tests/components/MCP3428/test.esp32-c3.yaml new file mode 100644 index 0000000000..a873e6e7f1 --- /dev/null +++ b/tests/components/MCP3428/test.esp32-c3.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp3428 + scl: 5 + sda: 4 + +mcp3428: + address: 0b1101000 + +sensor: + - platform: mcp3428 + multiplexer: 1 + id: mcp3428_sensor diff --git a/tests/components/MCP3428/test.esp32-idf.yaml.yaml b/tests/components/MCP3428/test.esp32-idf.yaml.yaml new file mode 100644 index 0000000000..a1ba9f0129 --- /dev/null +++ b/tests/components/MCP3428/test.esp32-idf.yaml.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp3428 + scl: 16 + sda: 17 + +mcp3428: + address: 0b1101000 + +sensor: + - platform: mcp3428 + multiplexer: 1 + id: mcp3428_sensor diff --git a/tests/components/MCP3428/test.esp32.yaml b/tests/components/MCP3428/test.esp32.yaml new file mode 100644 index 0000000000..a1ba9f0129 --- /dev/null +++ b/tests/components/MCP3428/test.esp32.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp3428 + scl: 16 + sda: 17 + +mcp3428: + address: 0b1101000 + +sensor: + - platform: mcp3428 + multiplexer: 1 + id: mcp3428_sensor diff --git a/tests/components/MCP3428/test.esp8266.yaml b/tests/components/MCP3428/test.esp8266.yaml new file mode 100644 index 0000000000..a873e6e7f1 --- /dev/null +++ b/tests/components/MCP3428/test.esp8266.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp3428 + scl: 5 + sda: 4 + +mcp3428: + address: 0b1101000 + +sensor: + - platform: mcp3428 + multiplexer: 1 + id: mcp3428_sensor diff --git a/tests/components/MCP3428/test.rp2040.yaml b/tests/components/MCP3428/test.rp2040.yaml new file mode 100644 index 0000000000..a873e6e7f1 --- /dev/null +++ b/tests/components/MCP3428/test.rp2040.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_mcp3428 + scl: 5 + sda: 4 + +mcp3428: + address: 0b1101000 + +sensor: + - platform: mcp3428 + multiplexer: 1 + id: mcp3428_sensor