From 4aeacfd16e171f1914c9e60e881b9b760a8b1dea Mon Sep 17 00:00:00 2001 From: mckaymatthew Date: Tue, 8 Feb 2022 03:56:40 -0600 Subject: [PATCH] Add max9611 High Side Current Shunt ADC (#2705) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/max9611/__init__.py | 1 + esphome/components/max9611/max9611.cpp | 93 ++++++++++++++++++++++++++ esphome/components/max9611/max9611.h | 62 +++++++++++++++++ esphome/components/max9611/sensor.py | 92 +++++++++++++++++++++++++ tests/test1.yaml | 16 ++++- 6 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 esphome/components/max9611/__init__.py create mode 100644 esphome/components/max9611/max9611.cpp create mode 100644 esphome/components/max9611/max9611.h create mode 100644 esphome/components/max9611/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 3326b7d90c..165ce52485 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -94,6 +94,7 @@ esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/ltr390/* @sjtrny esphome/components/max7219digit/* @rspaargaren +esphome/components/max9611/* @mckaymatthew esphome/components/mcp23008/* @jesserockz esphome/components/mcp23017/* @jesserockz esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz diff --git a/esphome/components/max9611/__init__.py b/esphome/components/max9611/__init__.py new file mode 100644 index 0000000000..9b0ea14b57 --- /dev/null +++ b/esphome/components/max9611/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@mckaymatthew"] diff --git a/esphome/components/max9611/max9611.cpp b/esphome/components/max9611/max9611.cpp new file mode 100644 index 0000000000..418a577948 --- /dev/null +++ b/esphome/components/max9611/max9611.cpp @@ -0,0 +1,93 @@ +#include "max9611.h" +#include "esphome/core/log.h" +#include "esphome/components/i2c/i2c_bus.h" +namespace esphome { +namespace max9611 { +using namespace esphome::i2c; +// Sign extend +// http://graphics.stanford.edu/~seander/bithacks.html#FixedSignExtend +template inline T signextend(const T x) { + struct { + T x : B; + } s; + return s.x = x; +} +// Map the gain register to in uV/LSB +const float gain_to_lsb(MAX9611Multiplexer gain) { + float lsb = 0.0; + if (gain == MAX9611_MULTIPLEXER_CSA_GAIN1) { + lsb = 107.50; + } else if (gain == MAX9611_MULTIPLEXER_CSA_GAIN4) { + lsb = 26.88; + } else if (gain == MAX9611_MULTIPLEXER_CSA_GAIN8) { + lsb = 13.44; + } + return lsb; +} +static const char *const TAG = "max9611"; +static const uint8_t SETUP_DELAY = 4; // Wait 2 integration periods. +static const float VOUT_LSB = 14.0 / 1000.0; // 14mV/LSB +static const float TEMP_LSB = 0.48; // 0.48C/LSB +static const float MICRO_VOLTS_PER_VOLT = 1000000.0; +void MAX9611Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up max9611..."); + // Perform dummy-read + uint8_t value; + this->read(&value, 1); + // Configuration Stage. + // First send an integration request with the specified gain + const uint8_t setup_dat[] = {CONTROL_REGISTER_1_ADRR, static_cast(gain_)}; + // Then send a request that samples all channels as fast as possible, using the last provided gain + const uint8_t fast_mode_dat[] = {CONTROL_REGISTER_1_ADRR, MAX9611Multiplexer::MAX9611_MULTIPLEXER_FAST_MODE}; + + if (this->write(reinterpret_cast(&setup_dat), sizeof(setup_dat)) != ErrorCode::ERROR_OK) { + ESP_LOGE(TAG, "Failed to setup Max9611 during GAIN SET"); + return; + } + delay(SETUP_DELAY); + if (this->write(reinterpret_cast(&fast_mode_dat), sizeof(fast_mode_dat)) != ErrorCode::ERROR_OK) { + ESP_LOGE(TAG, "Failed to setup Max9611 during FAST MODE SET"); + return; + } +} +void MAX9611Component::dump_config() { + ESP_LOGCONFIG(TAG, "Dump Config max9611..."); + ESP_LOGCONFIG(TAG, " CSA Gain Register: %x", gain_); + LOG_I2C_DEVICE(this); +} +void MAX9611Component::update() { + // Setup read from 0x0 register base + const uint8_t reg_base = 0x0; + const ErrorCode write_result = this->write(®_base, 1); + // Just read the entire register map in a bulk read, faster than individually querying register. + const ErrorCode read_result = this->read(register_map_, sizeof(register_map_)); + if (write_result != ErrorCode::ERROR_OK || read_result != ErrorCode::ERROR_OK) { + ESP_LOGW(TAG, "MAX9611 Update FAILED!"); + return; + } + uint16_t csa_register = ((register_map_[CSA_DATA_BYTE_MSB_ADRR] << 8) | (register_map_[CSA_DATA_BYTE_LSB_ADRR])) >> 4; + uint16_t rs_register = ((register_map_[RS_DATA_BYTE_MSB_ADRR] << 8) | (register_map_[RS_DATA_BYTE_LSB_ADRR])) >> 4; + uint16_t t_register = ((register_map_[TEMP_DATA_BYTE_MSB_ADRR] << 8) | (register_map_[TEMP_DATA_BYTE_LSB_ADRR])) >> 7; + float voltage = rs_register * VOUT_LSB; + float shunt_voltage = (csa_register * gain_to_lsb(gain_)) / MICRO_VOLTS_PER_VOLT; + float temp = signextend(t_register) * TEMP_LSB; + float amps = shunt_voltage / current_resistor_; + float watts = amps * voltage; + + if (voltage_sensor_ != nullptr) { + voltage_sensor_->publish_state(voltage); + } + if (current_sensor_ != nullptr) { + current_sensor_->publish_state(amps); + } + if (watt_sensor_ != nullptr) { + watt_sensor_->publish_state(watts); + } + if (temperature_sensor_ != nullptr) { + temperature_sensor_->publish_state(temp); + } + + ESP_LOGD(TAG, "V: %f, A: %f, W: %f, Deg C: %f", voltage, amps, watts, temp); +} +} // namespace max9611 +} // namespace esphome diff --git a/esphome/components/max9611/max9611.h b/esphome/components/max9611/max9611.h new file mode 100644 index 0000000000..017f56b1a7 --- /dev/null +++ b/esphome/components/max9611/max9611.h @@ -0,0 +1,62 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace max9611 { + +enum MAX9611Multiplexer { + MAX9611_MULTIPLEXER_CSA_GAIN1 = 0b000, + MAX9611_MULTIPLEXER_CSA_GAIN4 = 0b001, + MAX9611_MULTIPLEXER_CSA_GAIN8 = 0b010, + MAX9611_MULTIPLEXER_RS = 0b011, + MAX9611_MULTIPLEXER_OUT = 0b100, + MAX9611_MULTIPLEXER_SET = 0b101, + MAX9611_MULTIPLEXER_TEMP = 0b110, + MAX9611_MULTIPLEXER_FAST_MODE = 0b111, +}; + +enum MAX9611RegisterMap { + CSA_DATA_BYTE_MSB_ADRR = 0x00, + CSA_DATA_BYTE_LSB_ADRR = 0x01, + RS_DATA_BYTE_MSB_ADRR = 0x02, + RS_DATA_BYTE_LSB_ADRR = 0x03, + OUT_DATA_BYTE_MSB_ADRR = 0x04, // Unused Op-Amp + OUT_DATA_BYTE_LSB_ADRR = 0x05, // Unused Op-Amp + SET_DATA_BYTE_MSB_ADRR = 0x06, // Unused Op-Amp + SET_DATA_BYTE_LSB_ADRR = 0x07, // Unused Op-Amp + TEMP_DATA_BYTE_MSB_ADRR = 0x08, + TEMP_DATA_BYTE_LSB_ADRR = 0x09, + CONTROL_REGISTER_1_ADRR = 0x0A, + CONTROL_REGISTER_2_ADRR = 0x0B, +}; + +class MAX9611Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void update() override; + void set_voltage_sensor(sensor::Sensor *vs) { voltage_sensor_ = vs; } + void set_current_sensor(sensor::Sensor *cs) { current_sensor_ = cs; } + void set_watt_sensor(sensor::Sensor *ws) { watt_sensor_ = ws; } + void set_temp_sensor(sensor::Sensor *ts) { temperature_sensor_ = ts; } + + void set_current_resistor(float r) { current_resistor_ = r; } + void set_gain(MAX9611Multiplexer g) { gain_ = g; } + + protected: + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *watt_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + float current_resistor_; + uint8_t register_map_[0x0C]; + MAX9611Multiplexer gain_; +}; + +} // namespace max9611 +} // namespace esphome diff --git a/esphome/components/max9611/sensor.py b/esphome/components/max9611/sensor.py new file mode 100644 index 0000000000..246d332a86 --- /dev/null +++ b/esphome/components/max9611/sensor.py @@ -0,0 +1,92 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_SHUNT_RESISTANCE, + CONF_GAIN, + CONF_VOLTAGE, + CONF_CURRENT, + CONF_POWER, + CONF_TEMPERATURE, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + UNIT_CELSIUS, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, +) + +DEPENDENCIES = ["i2c"] +max9611_ns = cg.esphome_ns.namespace("max9611") +max9611Gain = max9611_ns.enum("MAX9611Multiplexer") +MAX9611_GAIN = { + "8X": max9611Gain.MAX9611_MULTIPLEXER_CSA_GAIN8, + "4X": max9611Gain.MAX9611_MULTIPLEXER_CSA_GAIN4, + "1X": max9611Gain.MAX9611_MULTIPLEXER_CSA_GAIN1, +} +MAX9611Component = max9611_ns.class_( + "MAX9611Component", cg.PollingComponent, i2c.I2CDevice +) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MAX9611Component), + cv.Required(CONF_SHUNT_RESISTANCE): cv.resistance, + cv.Required(CONF_GAIN): cv.enum(MAX9611_GAIN, upper=True), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x70)) +) + + +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_current_resistor(config[CONF_SHUNT_RESISTANCE])) + cg.add(var.set_gain(config[CONF_GAIN])) + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = await sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = await sensor.new_sensor(conf) + cg.add(var.set_watt_sensor(sens)) + if CONF_TEMPERATURE in config: + conf = config[CONF_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_temp_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index e97b3aed73..d8fea223dc 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1077,7 +1077,21 @@ sensor: cs_pin: mcp23xxx: mcp23017_hub number: 14 - + - platform: max9611 + i2c_id: i2c_bus + shunt_resistance: 0.2 ohm + gain: '1X' + voltage: + name: Max9611 Voltage + current: + name: Max9611 Current + power: + name: Max9611 Watts + temperature: + name: Max9611 Temp + update_interval: 1s + + esp32_touch: setup_mode: False iir_filter: 10ms