diff --git a/CODEOWNERS b/CODEOWNERS index 30be9e74b1..576af1459e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -29,6 +29,7 @@ esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet esphome/components/coolix/* @glmnet esphome/components/cover/* @esphome/core +esphome/components/cs5460a/* @balrog-kun esphome/components/ct_clamp/* @jesserockz esphome/components/debug/* @OttoWinter esphome/components/dfplayer/* @glmnet diff --git a/esphome/components/cs5460a/__init__.py b/esphome/components/cs5460a/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/cs5460a/cs5460a.cpp b/esphome/components/cs5460a/cs5460a.cpp new file mode 100644 index 0000000000..03cbd83513 --- /dev/null +++ b/esphome/components/cs5460a/cs5460a.cpp @@ -0,0 +1,342 @@ +#include "cs5460a.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace cs5460a { + +static const char *TAG = "cs5460a"; + +void CS5460AComponent::write_register_(enum CS5460ARegister addr, uint32_t value) { + this->write_byte(CMD_WRITE | (addr << 1)); + this->write_byte(value >> 16); + this->write_byte(value >> 8); + this->write_byte(value >> 0); +} + +uint32_t CS5460AComponent::read_register_(uint8_t addr) { + uint32_t value; + + this->write_byte(CMD_READ | (addr << 1)); + value = (uint32_t) this->transfer_byte(CMD_SYNC0) << 16; + value |= (uint32_t) this->transfer_byte(CMD_SYNC0) << 8; + value |= this->transfer_byte(CMD_SYNC0) << 0; + + return value; +} + +bool CS5460AComponent::softreset_() { + uint32_t pc = ((uint8_t) phase_offset_ & 0x3f) | (phase_offset_ < 0 ? 0x40 : 0); + uint32_t config = (1 << 0) | /* K = 0b0001 */ + (current_hpf_ ? 1 << 5 : 0) | /* IHPF */ + (voltage_hpf_ ? 1 << 6 : 0) | /* VHPF */ + (pga_gain_ << 16) | /* Gi */ + (pc << 17); /* PC */ + int cnt = 0; + + /* Serial resynchronization */ + this->write_byte(CMD_SYNC1); + this->write_byte(CMD_SYNC1); + this->write_byte(CMD_SYNC1); + this->write_byte(CMD_SYNC0); + + /* Reset */ + this->write_register_(REG_CONFIG, 1 << 7); + delay(10); + while (cnt++ < 50 && (this->read_register_(REG_CONFIG) & 0x81) != 0x000001) + ; + if (cnt > 50) + return false; + + this->write_register_(REG_CONFIG, config); + return true; +} + +void CS5460AComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up CS5460A..."); + + float current_full_scale = (pga_gain_ == CS5460A_PGA_GAIN_10X) ? 0.25 : 0.10; + float voltage_full_scale = 0.25; + current_multiplier_ = current_full_scale / (fabsf(current_gain_) * 0x1000000); + voltage_multiplier_ = voltage_full_scale / (voltage_gain_ * 0x1000000); + + /* + * Calculate power from the Energy register because the Power register + * stores instantaneous power which varies a lot in each AC cycle, + * while the Energy value is accumulated over the "computation cycle" + * which should be an integer number of AC cycles. + */ + power_multiplier_ = + (current_full_scale * voltage_full_scale * 4096) / (current_gain_ * voltage_gain_ * samples_ * 0x800000); + + pulse_freq_ = + (current_full_scale * voltage_full_scale) / (fabsf(current_gain_) * voltage_gain_ * pulse_energy_wh_ * 3600); + + hw_init_(); +} + +void CS5460AComponent::hw_init_() { + this->spi_setup(); + this->enable(); + + if (!this->softreset_()) { + this->disable(); + ESP_LOGE(TAG, "CS5460A reset failed!"); + this->mark_failed(); + return; + } + + uint32_t status = this->read_register_(REG_STATUS); + ESP_LOGCONFIG(TAG, " Version: %x", (status >> 6) & 7); + + this->write_register_(REG_CYCLE_COUNT, samples_); + this->write_register_(REG_PULSE_RATE, lroundf(pulse_freq_ * 32.0f)); + + /* Use one of the power saving features (assuming external oscillator), reset other CONTROL bits, + * sometimes softreset_() is not enough */ + this->write_register_(REG_CONTROL, 0x000004); + + this->restart_(); + this->disable(); + ESP_LOGCONFIG(TAG, " Init ok"); +} + +/* Doesn't reset the register values etc., just restarts the "computation cycle" */ +void CS5460AComponent::restart_() { + int cnt; + + this->enable(); + /* Stop running conversion, wake up if needed */ + this->write_byte(CMD_POWER_UP); + /* Start continuous conversion */ + this->write_byte(CMD_START_CONT); + this->disable(); + + this->started_(); +} + +void CS5460AComponent::started_() { + /* + * Try to guess when the next batch of results is going to be ready and + * schedule next STATUS check some time before that moment. This assumes + * two things: + * * a new "computation cycle" started just now. If it started some + * time ago we may be a late next time, but hopefully less late in each + * iteration -- that's why we schedule the next check in some 0.8 of + * the time we actually expect the next reading ready. + * * MCLK rate is 4.096MHz and K == 1. If there's a CS5460A module in + * use with a different clock this will need to be parametrised. + */ + expect_data_ts_ = millis() + samples_ * 1024 / 4096; + + schedule_next_check_(); +} + +void CS5460AComponent::schedule_next_check_() { + int32_t time_left = expect_data_ts_ - millis(); + + /* First try at 0.8 of the actual expected time (if it's in the future) */ + if (time_left > 0) + time_left -= time_left / 5; + + if (time_left > -500) { + /* But not sooner than in 30ms from now */ + if (time_left < 30) + time_left = 30; + } else { + /* + * If the measurement is more than 0.5s overdue start worrying. The + * device may be stuck because of an overcurrent error or similar, + * from now on just retry every 1s. After 15s try a reset, if it + * fails we give up and mark the component "failed". + */ + if (time_left > -15000) { + time_left = 1000; + this->status_momentary_warning("warning", 1000); + } else { + ESP_LOGCONFIG(TAG, "Device officially stuck, resetting"); + this->cancel_timeout("status-check"); + this->hw_init_(); + return; + } + } + + this->set_timeout("status-check", time_left, [this]() { + if (!this->check_status_()) + this->schedule_next_check_(); + }); +} + +bool CS5460AComponent::check_status_() { + this->enable(); + uint32_t status = this->read_register_(REG_STATUS); + + if (!(status & 0xcbf83c)) { + this->disable(); + return false; + } + + uint32_t clear = 1 << 20; + + /* TODO: Report if IC=0 but only once as it can't be cleared */ + + if (status & (1 << 2)) { + clear |= 1 << 2; + ESP_LOGE(TAG, "Low supply detected"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 3)) { + clear |= 1 << 3; + ESP_LOGE(TAG, "Modulator oscillation on current channel"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 4)) { + clear |= 1 << 4; + ESP_LOGE(TAG, "Modulator oscillation on voltage channel"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 5)) { + clear |= 1 << 5; + ESP_LOGE(TAG, "Watch-dog timeout"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 11)) { + clear |= 1 << 11; + ESP_LOGE(TAG, "EOUT Energy Accumulation Register out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 12)) { + clear |= 1 << 12; + ESP_LOGE(TAG, "Energy out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 13)) { + clear |= 1 << 13; + ESP_LOGE(TAG, "RMS voltage out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 14)) { + clear |= 1 << 14; + ESP_LOGE(TAG, "RMS current out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 15)) { + clear |= 1 << 15; + ESP_LOGE(TAG, "Power calculation out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 16)) { + clear |= 1 << 16; + ESP_LOGE(TAG, "Voltage out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 17)) { + clear |= 1 << 17; + ESP_LOGE(TAG, "Current out of range"); + this->status_momentary_warning("warning", 500); + } + + if (status & (1 << 19)) { + clear |= 1 << 19; + ESP_LOGE(TAG, "Divide overflowed"); + } + + if (status & (1 << 22)) { + bool dir = status & (1 << 21); + if (current_gain_ < 0) + dir = !dir; + ESP_LOGI(TAG, "Energy counter %s pulse", dir ? "negative" : "positive"); + clear |= 1 << 22; + } + + uint32_t raw_current = 0; /* Calm the validators */ + uint32_t raw_voltage = 0; + uint32_t raw_energy = 0; + + if (status & (1 << 23)) { + clear |= 1 << 23; + + if (current_sensor_ != nullptr) + raw_current = this->read_register_(REG_IRMS); + + if (voltage_sensor_ != nullptr) + raw_voltage = this->read_register_(REG_VRMS); + } + + if (status & ((1 << 23) | (1 << 5))) { + /* Read to clear the WDT bit */ + raw_energy = this->read_register_(REG_E); + } + + this->write_register_(REG_STATUS, clear); + this->disable(); + + /* + * Schedule the next STATUS check assuming that DRDY was asserted very + * recently, then publish the new values. Do this last for reentrancy in + * case the publish triggers a restart() or for whatever reason needs to + * cancel the timeout set in schedule_next_check_(), or needs to use SPI. + * If the current or power values haven't changed one bit it may be that + * the chip somehow forgot to update the registers -- seen happening very + * rarely. In that case don't publish them because the user may have + * the input connected to a multiplexer and may have switched channels + * since the previous reading and we'd be publishing the stale value for + * the new channel. If the value *was* updated it's very unlikely that + * it wouldn't have changed, especially power/energy which are affected + * by the noise on both the current and value channels (in case of energy, + * accumulated over many conversion cycles.) + */ + if (status & (1 << 23)) { + this->started_(); + + if (current_sensor_ != nullptr && raw_current != prev_raw_current_) { + current_sensor_->publish_state(raw_current * current_multiplier_); + prev_raw_current_ = raw_current; + } + + if (voltage_sensor_ != nullptr) + voltage_sensor_->publish_state(raw_voltage * voltage_multiplier_); + + if (power_sensor_ != nullptr && raw_energy != prev_raw_energy_) { + int32_t raw = (int32_t)(raw_energy << 8) >> 8; /* Sign-extend */ + power_sensor_->publish_state(raw * power_multiplier_); + prev_raw_energy_ = raw_energy; + } + + return true; + } + + return false; +} + +void CS5460AComponent::dump_config() { + uint32_t state = this->get_component_state(); + + ESP_LOGCONFIG(TAG, "CS5460A:"); + ESP_LOGCONFIG(TAG, " Init status: %s", + state == COMPONENT_STATE_LOOP ? "OK" : (state == COMPONENT_STATE_FAILED ? "failed" : "other")); + LOG_PIN(" CS Pin: ", cs_); + ESP_LOGCONFIG(TAG, " Samples / cycle: %u", samples_); + ESP_LOGCONFIG(TAG, " Phase offset: %i", phase_offset_); + ESP_LOGCONFIG(TAG, " PGA Gain: %s", pga_gain_ == CS5460A_PGA_GAIN_50X ? "50x" : "10x"); + ESP_LOGCONFIG(TAG, " Current gain: %.5f", current_gain_); + ESP_LOGCONFIG(TAG, " Voltage gain: %.5f", voltage_gain_); + ESP_LOGCONFIG(TAG, " Current HPF: %s", current_hpf_ ? "enabled" : "disabled"); + ESP_LOGCONFIG(TAG, " Voltage HPF: %s", voltage_hpf_ ? "enabled" : "disabled"); + ESP_LOGCONFIG(TAG, " Pulse energy: %.2f Wh", pulse_energy_wh_); + LOG_SENSOR(" ", "Voltage", voltage_sensor_); + LOG_SENSOR(" ", "Current", current_sensor_); + LOG_SENSOR(" ", "Power", power_sensor_); +} + +} // namespace cs5460a +} // namespace esphome diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h new file mode 100644 index 0000000000..699049757c --- /dev/null +++ b/esphome/components/cs5460a/cs5460a.h @@ -0,0 +1,123 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace cs5460a { + +enum CS5460ACommand { + CMD_SYNC0 = 0xfe, + CMD_SYNC1 = 0xff, + CMD_START_SINGLE = 0xe0, + CMD_START_CONT = 0xe8, + CMD_POWER_UP = 0xa0, + CMD_POWER_STANDBY = 0x88, + CMD_POWER_SLEEP = 0x90, + CMD_CALIBRATION = 0xc0, + CMD_READ = 0x00, + CMD_WRITE = 0x40, +}; + +enum CS5460ARegister { + REG_CONFIG = 0x00, + REG_IDCOFF = 0x01, + REG_IGN = 0x02, + REG_VDCOFF = 0x03, + REG_VGN = 0x04, + REG_CYCLE_COUNT = 0x05, + REG_PULSE_RATE = 0x06, + REG_I = 0x07, + REG_V = 0x08, + REG_P = 0x09, + REG_E = 0x0a, + REG_IRMS = 0x0b, + REG_VRMS = 0x0c, + REG_TBC = 0x0d, + REG_POFF = 0x0e, + REG_STATUS = 0x0f, + REG_IACOFF = 0x10, + REG_VACOFF = 0x11, + REG_MASK = 0x1a, + REG_CONTROL = 0x1c, +}; + +/** Enum listing the current channel aplifiergain settings for the CS5460A. + */ +enum CS5460APGAGain { + CS5460A_PGA_GAIN_10X = 0b0, + CS5460A_PGA_GAIN_50X = 0b1, +}; + +class CS5460AComponent : public Component, + public spi::SPIDevice { + public: + void set_samples(uint32_t samples) { samples_ = samples; } + void set_phase_offset(int8_t phase_offset) { phase_offset_ = phase_offset; } + void set_pga_gain(CS5460APGAGain pga_gain) { pga_gain_ = pga_gain; } + void set_gains(float current_gain, float voltage_gain) { + current_gain_ = current_gain; + voltage_gain_ = voltage_gain; + } + void set_hpf_enable(bool current_hpf, bool voltage_hpf) { + current_hpf_ = current_hpf; + voltage_hpf_ = voltage_hpf; + } + void set_pulse_energy_wh(float pulse_energy_wh) { pulse_energy_wh_ = pulse_energy_wh; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + + void restart() { restart_(); } + + void setup() override; + void loop() override {} + float get_setup_priority() const override { return setup_priority::DATA; } + void dump_config() override; + + protected: + uint32_t samples_; + int8_t phase_offset_; + CS5460APGAGain pga_gain_; + float current_gain_; + float voltage_gain_; + bool current_hpf_; + bool voltage_hpf_; + float pulse_energy_wh_; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + + void write_register_(enum CS5460ARegister addr, uint32_t value); + uint32_t read_register_(uint8_t addr); + bool softreset_(); + void hw_init_(); + void restart_(); + void started_(); + void schedule_next_check_(); + bool check_status_(); + + float current_multiplier_; + float voltage_multiplier_; + float power_multiplier_; + float pulse_freq_; + uint32_t expect_data_ts_; + uint32_t prev_raw_current_{0}; + uint32_t prev_raw_energy_{0}; +}; + +template class CS5460ARestartAction : public Action { + public: + CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {} + + void play(Ts... x) override { cs5460a_->restart(); } + + protected: + CS5460AComponent *cs5460a_; +}; + +} // namespace cs5460a +} // namespace esphome diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py new file mode 100644 index 0000000000..efb1d1d426 --- /dev/null +++ b/esphome/components/cs5460a/sensor.py @@ -0,0 +1,136 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, sensor +from esphome.const import ( + CONF_CURRENT, + CONF_ID, + CONF_POWER, + CONF_VOLTAGE, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + ICON_EMPTY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_VOLTAGE, +) +from esphome import automation +from esphome.automation import maybe_simple_id + +CODEOWNERS = ["@balrog-kun"] +DEPENDENCIES = ["spi"] + +cs5460a_ns = cg.esphome_ns.namespace("cs5460a") +CS5460APGAGain = cs5460a_ns.enum("CS5460APGAGain") +PGA_GAIN_OPTIONS = { + "10X": CS5460APGAGain.CS5460A_PGA_GAIN_10X, + "50X": CS5460APGAGain.CS5460A_PGA_GAIN_50X, +} + +CS5460AComponent = cs5460a_ns.class_("CS5460AComponent", spi.SPIDevice, cg.Component) +CS5460ARestartAction = cs5460a_ns.class_("CS5460ARestartAction", automation.Action) + +CONF_SAMPLES = "samples" +CONF_PHASE_OFFSET = "phase_offset" +CONF_PGA_GAIN = "pga_gain" +CONF_CURRENT_GAIN = "current_gain" +CONF_VOLTAGE_GAIN = "voltage_gain" +CONF_CURRENT_HPF = "current_hpf" +CONF_VOLTAGE_HPF = "voltage_hpf" +CONF_PULSE_ENERGY = "pulse_energy" + + +def validate_config(config): + current_gain = abs(config[CONF_CURRENT_GAIN]) * ( + 1.0 if config[CONF_PGA_GAIN] == "10X" else 5.0 + ) + voltage_gain = config[CONF_VOLTAGE_GAIN] + pulse_energy = config[CONF_PULSE_ENERGY] + + if current_gain == 0.0 or voltage_gain == 0.0: + raise cv.Invalid("The gains can't be zero") + + max_energy = (0.25 * 0.25 / 3600 / (2 ** -4)) / (voltage_gain * current_gain) + min_energy = (0.25 * 0.25 / 3600 / (2 ** 18)) / (voltage_gain * current_gain) + mech_min_energy = (0.25 * 0.25 / 3600 / 7.8) / (voltage_gain * current_gain) + if pulse_energy < min_energy or pulse_energy > max_energy: + raise cv.Invalid( + "For given current&voltage gains, the pulse energy must be between " + f"{min_energy} Wh and {max_energy} Wh and in mechanical counter mode " + f"between {mech_min_energy} Wh and {max_energy} Wh" + ) + + return config + + +validate_energy = cv.float_with_unit("energy", "(Wh|WH|wh)?", optional_unit=True) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CS5460AComponent), + cv.Optional(CONF_SAMPLES, default=4000): cv.int_range(min=1, max=0xFFFFFF), + cv.Optional(CONF_PHASE_OFFSET, default=0): cv.int_range(min=-64, max=63), + cv.Optional(CONF_PGA_GAIN, default="10X"): cv.enum( + PGA_GAIN_OPTIONS, upper=True + ), + cv.Optional(CONF_CURRENT_GAIN, default=0.001): cv.negative_one_to_one_float, + cv.Optional(CONF_VOLTAGE_GAIN, default=0.001): cv.zero_to_one_float, + cv.Optional(CONF_CURRENT_HPF, default=True): cv.boolean, + cv.Optional(CONF_VOLTAGE_HPF, default=True): cv.boolean, + cv.Optional(CONF_PULSE_ENERGY, default=10.0): validate_energy, + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 0, DEVICE_CLASS_VOLTAGE + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(cs_pin_required=False)), + validate_config, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + + cg.add(var.set_samples(config[CONF_SAMPLES])) + cg.add(var.set_phase_offset(config[CONF_PHASE_OFFSET])) + cg.add(var.set_pga_gain(config[CONF_PGA_GAIN])) + cg.add(var.set_gains(config[CONF_CURRENT_GAIN], config[CONF_VOLTAGE_GAIN])) + cg.add(var.set_hpf_enable(config[CONF_CURRENT_HPF], config[CONF_VOLTAGE_HPF])) + cg.add(var.set_pulse_energy_wh(config[CONF_PULSE_ENERGY])) + + 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_power_sensor(sens)) + + +@automation.register_action( + "cs5460a.restart", + CS5460ARestartAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(CS5460AComponent), + } + ), +) +async def restart_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/tests/test1.yaml b/tests/test1.yaml index 6f87166490..31bfc5e9f0 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -914,6 +914,28 @@ sensor: id: ph_ezo address: 99 unit_of_measurement: 'pH' + - platform: cs5460a + id: cs5460a1 + current: + name: "Socket current" + voltage: + name: "Mains voltage" + power: + name: "Socket power" + on_value: + then: + cs5460a.restart: cs5460a1 + samples: 1600 + pga_gain: 10X + current_gain: 0.01 + voltage_gain: 0.000573 + current_hpf: on + voltage_hpf: on + phase_offset: 20 + pulse_energy: 0.01 kWh + cs_pin: + mcp23xxx: mcp23017_hub + number: 14 esp32_touch: setup_mode: False