Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 105 additions & 3 deletions components/m5unit_scales/m5unit_scales.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "m5unit_scales.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cmath>

namespace esphome {
namespace m5unit_scales {
Expand Down Expand Up @@ -47,9 +48,14 @@ void M5UnitScalesComponent::dump_config() {
if (this->is_failed()) {
ESP_LOGCONFIG(TAG, " Connection failed!");
}
#ifdef USE_SENSOR
LOG_SENSOR(" ", "Weight", this->weight_sensor_);
LOG_SENSOR(" ", "Absolute Weight", this->absolute_weight_sensor_);
LOG_SENSOR(" ", "Raw ADC", this->raw_adc_sensor_);
#endif
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Button", this->button_sensor_);
#endif
}

float M5UnitScalesComponent::get_setup_priority() const {
Expand All @@ -65,14 +71,17 @@ void M5UnitScalesComponent::update() {
if (this->model_ == M5UNIT_SCALES_MODEL_MINI && !this->initial_sync_done_) {
this->initial_sync_done_ = true;

#ifdef USE_SWITCH
// LP Filter State
uint8_t lp_val = 0;
if (this->read_byte(REG_MINI_FILTER_BASE, &lp_val)) {
if (this->lp_filter_switch_ != nullptr) {
this->lp_filter_switch_->publish_state(lp_val != 0);
}
}
#endif

#ifdef USE_NUMBER
// Avg Filter Value
uint8_t avg_val = 0;
if (this->read_byte(REG_MINI_FILTER_BASE + 1, &avg_val)) {
Expand All @@ -98,27 +107,39 @@ void M5UnitScalesComponent::update() {
this->gap_number_->publish_state(gap);
}
}
#endif
} else if (this->model_ == M5UNIT_SCALES_MODEL_STANDARD && !this->initial_sync_done_) {
this->initial_sync_done_ = true;
#ifdef USE_SWITCH
if (this->lp_filter_switch_ != nullptr) {
ESP_LOGW(TAG, "Low Pass Filter switch is only supported on MINI model.");
}
#endif
#ifdef USE_NUMBER
if (this->avg_filter_number_ != nullptr || this->ema_filter_number_ != nullptr) {
ESP_LOGW(TAG, "Average and EMA filter numbers are only supported on MINI model.");
}
if (this->gap_number_ != nullptr) {
ESP_LOGW(TAG, "Gap calibration number is only supported on MINI model. Use manual tare/zero calibration for STANDARD.");
}
#endif
}

#ifdef USE_SENSOR
// Read Weight
if (this->weight_sensor_ != nullptr) {
if (this->weight_sensor_ != nullptr || this->absolute_weight_sensor_ != nullptr) {
uint8_t data[4];
if (this->model_ == M5UNIT_SCALES_MODEL_MINI) {
if (this->read_bytes(REG_MINI_CAL_DATA, data, 4)) {
float weight = 0.0f;
memcpy(&weight, data, 4);
this->weight_sensor_->publish_state(weight);
this->last_read_weight_ = weight;
if (this->weight_sensor_ != nullptr) {
this->weight_sensor_->publish_state(weight);
}
if (this->absolute_weight_sensor_ != nullptr) {
this->absolute_weight_sensor_->publish_state(weight + this->tare_offset_);
}
} else {
ESP_LOGW(TAG, "Failed to read weight data.");
}
Expand All @@ -130,7 +151,13 @@ void M5UnitScalesComponent::update() {
((int32_t)data[2] << 8) |
data[3];
float weight = (float)weight_raw / 100.0f;
this->weight_sensor_->publish_state(weight);
this->last_read_weight_ = weight;
if (this->weight_sensor_ != nullptr) {
this->weight_sensor_->publish_state(weight);
}
if (this->absolute_weight_sensor_ != nullptr) {
this->absolute_weight_sensor_->publish_state(weight + this->tare_offset_);
}
} else {
ESP_LOGW(TAG, "Failed to read weight data.");
}
Expand Down Expand Up @@ -160,9 +187,11 @@ void M5UnitScalesComponent::update() {
}
}
}
#endif
}

void M5UnitScalesComponent::loop() {
#ifdef USE_BINARY_SENSOR
if (this->is_failed() || this->button_sensor_ == nullptr) {
return;
}
Expand All @@ -176,13 +205,86 @@ void M5UnitScalesComponent::loop() {
if (this->read_byte(reg, &btn_val)) {
// 0 = pressed, 1 = no press
bool pressed = (btn_val == 0);

// Detect rising edge of physical button press
if (pressed && !this->button_sensor_->state) {
// Physical button transition to pressed.
// The hardware will trigger a tare internally. Update tare_offset_ in software.
float current_weight = 0.0f;
bool read_success = false;
uint8_t data[4];
if (this->model_ == M5UNIT_SCALES_MODEL_MINI) {
if (this->read_bytes(REG_MINI_CAL_DATA, data, 4)) {
memcpy(&current_weight, data, 4);
read_success = true;
}
} else {
if (this->read_bytes(REG_STD_CAL_DATA, data, 4)) {
int32_t weight_raw = ((int32_t)data[0] << 24) |
((int32_t)data[1] << 16) |
((int32_t)data[2] << 8) |
data[3];
current_weight = (float)weight_raw / 100.0f;
read_success = true;
}
}

float weight_to_add = 0.0f;
if (read_success) {
// If the hardware hasn't zeroed the value yet, we use the on-demand read value.
// However, if the hardware already tared and returns ~0, but our last_read_weight_ was significant,
// we fall back to last_read_weight_.
if (std::abs(current_weight) < 0.01f && std::abs(this->last_read_weight_) > 0.1f) {
weight_to_add = this->last_read_weight_;
} else {
weight_to_add = current_weight;
}
} else {
weight_to_add = this->last_read_weight_;
}

this->tare_offset_ += weight_to_add;
ESP_LOGD(TAG, "Physical button pressed, updated tare offset to %.2f (added %.2f)", this->tare_offset_, weight_to_add);
}

this->button_sensor_->publish_state(pressed);
}
}
#endif
}

void M5UnitScalesComponent::tare() {
ESP_LOGI(TAG, "Executing tare / zero calibration...");

// Read current weight before tare command to update tare_offset_ in software
float current_weight = 0.0f;
bool read_success = false;
uint8_t data[4];
if (this->model_ == M5UNIT_SCALES_MODEL_MINI) {
if (this->read_bytes(REG_MINI_CAL_DATA, data, 4)) {
memcpy(&current_weight, data, 4);
read_success = true;
}
} else {
if (this->read_bytes(REG_STD_CAL_DATA, data, 4)) {
int32_t weight_raw = ((int32_t)data[0] << 24) |
((int32_t)data[1] << 16) |
((int32_t)data[2] << 8) |
data[3];
current_weight = (float)weight_raw / 100.0f;
read_success = true;
}
}

if (read_success) {
this->tare_offset_ += current_weight;
ESP_LOGD(TAG, "Software tare triggered, updated tare offset to %.2f (added %.2f)", this->tare_offset_, current_weight);
} else {
// If on-demand read failed, fall back to last_read_weight_
this->tare_offset_ += this->last_read_weight_;
ESP_LOGW(TAG, "Failed to read current weight synchronously, fell back to last read weight: %.2f", this->last_read_weight_);
}

uint8_t cmd = 1;
uint8_t reg = (this->model_ == M5UNIT_SCALES_MODEL_MINI) ? REG_MINI_SET_OFFSET : REG_STD_SET_OFFSET;
if (!this->write_byte(reg, cmd)) {
Expand Down
54 changes: 53 additions & 1 deletion components/m5unit_scales/m5unit_scales.h
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
#pragma once

#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/components/i2c/i2c.h"

#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_BUTTON
#include "esphome/components/button/button.h"
#endif
#ifdef USE_LIGHT
#include "esphome/components/light/light_output.h"
#include "esphome/components/light/light_state.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#include "esphome/components/i2c/i2c.h"
#endif

namespace esphome {
namespace m5unit_scales {
Expand All @@ -30,17 +44,30 @@ class M5UnitScalesComponent : public PollingComponent, public i2c::I2CDevice {
void set_model(M5UnitScalesModel model) { model_ = model; }

// Setters for platform items
#ifdef USE_SENSOR
void set_weight_sensor(sensor::Sensor *weight_sensor) { weight_sensor_ = weight_sensor; }
void set_raw_adc_sensor(sensor::Sensor *raw_adc_sensor) { raw_adc_sensor_ = raw_adc_sensor; }
void set_absolute_weight_sensor(sensor::Sensor *absolute_weight_sensor) { absolute_weight_sensor_ = absolute_weight_sensor; }
#endif
#ifdef USE_BINARY_SENSOR
void set_button_sensor(binary_sensor::BinarySensor *button_sensor) { button_sensor_ = button_sensor; }
#endif

// Custom platform wrappers
#ifdef USE_BUTTON
void set_tare_button(button::Button *tare_button) { tare_button_ = tare_button; }
#endif
#ifdef USE_SWITCH
void set_lp_filter_switch(switch_::Switch *lp_filter_switch) { lp_filter_switch_ = lp_filter_switch; }
#endif
#ifdef USE_NUMBER
void set_avg_filter_number(number::Number *avg_filter_number) { avg_filter_number_ = avg_filter_number; }
void set_ema_filter_number(number::Number *ema_filter_number) { ema_filter_number_ = ema_filter_number; }
void set_gap_number(number::Number *gap_number) { gap_number_ = gap_number; }
#endif
#ifdef USE_LIGHT
void set_led_light(light::LightOutput *led_light) { led_light_ = led_light; }
#endif

// API methods called by wrapper entities
void tare();
Expand All @@ -53,30 +80,50 @@ class M5UnitScalesComponent : public PollingComponent, public i2c::I2CDevice {
protected:
M5UnitScalesModel model_{M5UNIT_SCALES_MODEL_MINI};

#ifdef USE_SENSOR
sensor::Sensor *weight_sensor_{nullptr};
sensor::Sensor *raw_adc_sensor_{nullptr};
sensor::Sensor *absolute_weight_sensor_{nullptr};
#endif

float tare_offset_{0.0f};
float last_read_weight_{0.0f};

#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *button_sensor_{nullptr};
#endif
#ifdef USE_BUTTON
button::Button *tare_button_{nullptr};
#endif
#ifdef USE_SWITCH
switch_::Switch *lp_filter_switch_{nullptr};
#endif
#ifdef USE_NUMBER
number::Number *avg_filter_number_{nullptr};
number::Number *ema_filter_number_{nullptr};
number::Number *gap_number_{nullptr};
#endif
#ifdef USE_LIGHT
light::LightOutput *led_light_{nullptr};
#endif

uint32_t last_button_poll_{0};
bool initial_sync_done_{false};
};

// C++ Platform Wrappers

#ifdef USE_BUTTON
class M5UnitScalesTareButton : public button::Button {
public:
void set_parent(M5UnitScalesComponent *parent) { parent_ = parent; }
void press_action() override { this->parent_->tare(); }
protected:
M5UnitScalesComponent *parent_;
};
#endif

#ifdef USE_SWITCH
class M5UnitScalesLPFilterSwitch : public switch_::Switch {
public:
void set_parent(M5UnitScalesComponent *parent) { parent_ = parent; }
Expand All @@ -87,7 +134,9 @@ class M5UnitScalesLPFilterSwitch : public switch_::Switch {
protected:
M5UnitScalesComponent *parent_;
};
#endif

#ifdef USE_NUMBER
class M5UnitScalesAvgFilterNumber : public number::Number {
public:
void set_parent(M5UnitScalesComponent *parent) { parent_ = parent; }
Expand Down Expand Up @@ -120,7 +169,9 @@ class M5UnitScalesGapNumber : public number::Number {
protected:
M5UnitScalesComponent *parent_;
};
#endif

#ifdef USE_LIGHT
class M5UnitScalesLED : public light::LightOutput {
public:
void set_parent(M5UnitScalesComponent *parent) { parent_ = parent; }
Expand All @@ -140,6 +191,7 @@ class M5UnitScalesLED : public light::LightOutput {
protected:
M5UnitScalesComponent *parent_;
};
#endif

} // namespace m5unit_scales
} // namespace esphome
13 changes: 13 additions & 0 deletions components/m5unit_scales/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

CONF_WEIGHT = "weight"
CONF_RAW_ADC = "raw_adc"
CONF_ABSOLUTE_WEIGHT = "absolute_weight"

CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(CONF_M5UNIT_SCALES_ID): cv.use_id(M5UnitScalesComponent),
Expand All @@ -16,10 +17,19 @@
accuracy_decimals=2,
device_class=DEVICE_CLASS_WEIGHT,
state_class=STATE_CLASS_MEASUREMENT,
icon="mdi:scale-balance",
),
cv.Optional(CONF_RAW_ADC): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
icon="mdi:sine-wave",
),
cv.Optional(CONF_ABSOLUTE_WEIGHT): sensor.sensor_schema(
unit_of_measurement="g",
accuracy_decimals=2,
device_class=DEVICE_CLASS_WEIGHT,
state_class=STATE_CLASS_MEASUREMENT,
icon="mdi:scale-balance",
),
})

Expand All @@ -31,3 +41,6 @@ async def to_code(config):
if CONF_RAW_ADC in config:
sens = await sensor.new_sensor(config[CONF_RAW_ADC])
cg.add(parent.set_raw_adc_sensor(sens))
if CONF_ABSOLUTE_WEIGHT in config:
sens = await sensor.new_sensor(config[CONF_ABSOLUTE_WEIGHT])
cg.add(parent.set_absolute_weight_sensor(sens))
2 changes: 2 additions & 0 deletions tests/components/m5unit_scales/common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ sensor:
- platform: m5unit_scales
weight:
name: "Scale Weight"
absolute_weight:
name: "Scale Absolute Weight"
raw_adc:
name: "Scale Raw ADC"

Expand Down