Add an ability to calibrate Shelly Dimmer 2

This commit is contained in:
Oleg Tarasov 2024-11-15 17:54:33 +03:00
parent e819185de1
commit 8ab3f386d5
2 changed files with 176 additions and 8 deletions

View file

@ -21,6 +21,9 @@ namespace {
constexpr char TAG[] = "shelly_dimmer";
constexpr float CALIBRATION_STEP = 0.05f;
constexpr uint32_t RESTORE_STATE_VERSION = 0x362A4931UL;
constexpr uint8_t SHELLY_DIMMER_ACK_TIMEOUT = 200; // ms
constexpr uint8_t SHELLY_DIMMER_MAX_RETRIES = 3;
constexpr uint16_t SHELLY_DIMMER_MAX_BRIGHTNESS = 1000; // 100%
@ -109,10 +112,26 @@ void ShellyDimmer::setup() {
// Do an immediate poll to refresh current state.
this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0);
this->calibration_data_.fill(0);
this->rtc_ = global_preferences->make_preference<std::array<float, 20>>(this->state_->get_object_id_hash() ^
RESTORE_STATE_VERSION);
if (this->rtc_.load(&this->calibration_data_)) {
ESP_LOGD(TAG, "Loaded calibration from flash");
for (float value : this->calibration_data_) {
ESP_LOGV(TAG, "%f", value);
}
}
this->ready_ = true;
}
void ShellyDimmer::update() { this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); }
void ShellyDimmer::update() {
this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0);
if (this->calibrating_) {
this->perform_calibration_measurement_();
}
}
void ShellyDimmer::dump_config() {
ESP_LOGCONFIG(TAG, "ShellyDimmer:");
@ -131,6 +150,7 @@ void ShellyDimmer::dump_config() {
ESP_LOGCONFIG(TAG, " STM32 current firmware version: %d.%d ", this->version_major_, this->version_minor_);
ESP_LOGCONFIG(TAG, " STM32 required firmware version: %d.%d", USE_SHD_FIRMWARE_MAJOR_VERSION,
USE_SHD_FIRMWARE_MINOR_VERSION);
ESP_LOGCONFIG(TAG, " Calibrated: %s", YESNO(this->calibration_data_[0] != 0));
if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION ||
this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) {
@ -146,6 +166,32 @@ void ShellyDimmer::write_state(light::LightState *state) {
float brightness;
state->current_values_as_brightness(&brightness);
// If we are in a process of calibration, don't mess with brightness.
// Also check whether we have calibration data and wheteher edge values were requested.
if (!this->calibrating_ && this->calibration_data_[0] != 0.0f && brightness != 0 && brightness != 1.0f) {
// We have calibration data, find the nearest range and remap value
int pos;
for (pos = 0; pos < this->calibration_data_.size(); ++pos) {
if (this->calibration_data_[pos] < brightness) {
break;
}
}
if (pos == this->calibration_data_.size() || pos == 0) {
ESP_LOGW(TAG, "Failed to find suitable calibration range for brightness %f", brightness);
} else {
float min = this->calibration_data_[pos];
float max = this->calibration_data_[pos - 1];
float min_out = 1 - (float) pos * CALIBRATION_STEP;
float max_out = min_out + CALIBRATION_STEP;
float remapped = remap(brightness, min, max, min_out, max_out);
ESP_LOGD(TAG, "Remapped %f to %f (min %f, max %f, min_out %f, max_out %f)", brightness, remapped, min, max,
min_out, max_out);
brightness = remapped;
}
}
const uint16_t brightness_int = this->convert_brightness_(brightness);
if (brightness_int == this->brightness_) {
ESP_LOGV(TAG, "Not sending unchanged value");
@ -434,13 +480,13 @@ bool ShellyDimmer::handle_frame_() {
current = CURRENT_SCALING_FACTOR / static_cast<float>(current_raw);
}
ESP_LOGI(TAG, "Got dimmer data:");
ESP_LOGI(TAG, " HW version: %d", hw_version);
ESP_LOGI(TAG, " Brightness: %d", brightness);
ESP_LOGI(TAG, " Fade rate: %d", fade_rate);
ESP_LOGI(TAG, " Power: %f W", power);
ESP_LOGI(TAG, " Voltage: %f V", voltage);
ESP_LOGI(TAG, " Current: %f A", current);
ESP_LOGD(TAG, "Got dimmer data:");
ESP_LOGD(TAG, " HW version: %d", hw_version);
ESP_LOGD(TAG, " Brightness: %d", brightness);
ESP_LOGD(TAG, " Fade rate: %d", fade_rate);
ESP_LOGD(TAG, " Power: %f W", power);
ESP_LOGD(TAG, " Voltage: %f V", voltage);
ESP_LOGD(TAG, " Current: %f A", current);
// Update sensors.
if (this->power_sensor_ != nullptr) {
@ -521,6 +567,106 @@ void ShellyDimmer::reset_dfu_boot_() {
this->reset_(true);
}
void ShellyDimmer::start_calibration() {
ESP_LOGI(TAG, "Starting calibration");
// Turn on the light, disable transition, set max brightness
this->set_brightness_no_transition_(1);
// Init calibration data
this->calibration_measurements_.fill(0);
this->calibration_measurement_cnt_ = 0;
this->calibration_step_ = -20;
this->calibrating_ = true;
}
void ShellyDimmer::perform_calibration_measurement_() {
if (!this->power_sensor_->has_state()) // Wait for power sensor to receive data
return;
if (this->calibration_step_ < 0) {
ESP_LOGD(TAG, "Calibration warmup. Steps till calibration: %d", -this->calibration_step_);
this->calibration_step_++;
if (this->calibration_step_ == 0) {
ESP_LOGD(TAG, "Calibration warmup complete");
}
return;
}
ESP_LOGD(TAG, "Calibration step %d, measurement %d", this->calibration_step_ + 1,
this->calibration_measurement_cnt_ + 1);
this->calibration_measurements_[this->calibration_measurement_cnt_] = this->power_sensor_->state;
this->calibration_measurement_cnt_++;
if (this->calibration_measurement_cnt_ >= this->calibration_measurements_.size()) {
this->complete_calibration_step_();
}
}
void ShellyDimmer::complete_calibration_step_() {
// Calculate mean power across measurements at this step
float result = 0;
for (float measurement : this->calibration_measurements_) {
result += measurement;
}
result = result / (float) this->calibration_measurements_.size();
ESP_LOGD(TAG, "Mean power at step %d: %f", this->calibration_step_ + 1, result);
// Prepare for the next measurement
this->calibration_data_[this->calibration_step_] = result;
this->calibration_step_++;
this->calibration_measurement_cnt_ = 0;
this->calibration_measurements_.fill(0);
// If all measurements collected, finish calibration
if (this->calibration_step_ >= this->calibration_data_.size()) {
this->complete_calibration_();
return;
}
// Decrease brightness for next set of measurements
float cur_brightness = 0;
this->state_->current_values_as_brightness(&cur_brightness);
this->set_brightness_no_transition_(cur_brightness - CALIBRATION_STEP);
}
void ShellyDimmer::complete_calibration_() {
this->calibrating_ = false;
// Sort the values, since power readings can be jittery due to voltage fluctuations
std::sort(this->calibration_data_.begin(), this->calibration_data_.end(), std::greater{});
// Normalize values in the range of [0..1]
float max = this->calibration_data_[0];
float min = this->calibration_data_[this->calibration_data_.size() - 1];
for (float &value : this->calibration_data_) {
value = remap(value, min, max, 0.0f, 1.0f);
}
if (this->rtc_.save(&this->calibration_data_)) {
ESP_LOGD(TAG, "Saved calibration to flash");
} else {
ESP_LOGW(TAG, "Couldn't save calibration to flash");
}
ESP_LOGD(TAG, "Finished calibration. Values:");
for (float value : this->calibration_data_) {
ESP_LOGD(TAG, "%f", value);
}
this->set_brightness_no_transition_(1);
}
void ShellyDimmer::set_brightness_no_transition_(float brightness) {
auto call = this->state_->make_call();
call.set_brightness(brightness);
call.set_transition_length(0);
call.set_state(true);
call.perform();
}
} // namespace shelly_dimmer
} // namespace esphome

View file

@ -8,6 +8,7 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include <algorithm>
#include <array>
namespace esphome {
@ -49,6 +50,8 @@ class ShellyDimmer : public PollingComponent, public light::LightOutput, public
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
void start_calibration();
protected:
GPIOPin *pin_nrst_;
GPIOPin *pin_boot0_;
@ -77,6 +80,13 @@ class ShellyDimmer : public PollingComponent, public light::LightOutput, public
bool ready_{false};
uint16_t brightness_;
bool calibrating_{false};
int8_t calibration_step_{0};
uint8_t calibration_measurement_cnt_{0};
std::array<float, 3> calibration_measurements_;
std::array<float, 20> calibration_data_;
ESPPreferenceObject rtc_;
/// Convert relative brightness into a dimmer brightness value.
uint16_t convert_brightness_(float brightness);
@ -115,6 +125,18 @@ class ShellyDimmer : public PollingComponent, public light::LightOutput, public
/// Reset STM32 to boot into DFU mode to enable firmware upgrades.
void reset_dfu_boot_();
/// Perform calibration measurement.
void perform_calibration_measurement_();
/// Complete a single calibration step averaging over accumulated measurements.
void complete_calibration_step_();
// Complete the whole calibration process.
void complete_calibration_();
// Set brightness with no transition during calibration.
void set_brightness_no_transition_(float brightness);
};
} // namespace shelly_dimmer