Hi,
After developing my own ESPhome library for the various devices I am working with (mainly M5Stack) I took the plunge to write a custom component for it to abstract away the various specific settings and mappings based on the M5Unified library from M5Stack. The goal is to natively support all or at least most M5Stack devices out of the box.
I have the basics up and running and have been playing with m5stack-core2, m5stack-c3u, m5stick-c, and m5tough. My custom M5Unified component so far handles the board basics like mapping the various Power chips (AXP192&Co), IMUs, RTC, SPK, MIC and more, all courtesy of the M5Stack Unified libraries.
To do so, I created a configurable M5Unified class and configuration mapping that sets up the unified support:
class M5Unified : public Component {
public:
M5Unified() = default;
float get_setup_priority() const override;
void setup() override;
void dump_config() override;
void loop() override;
void set_serial_baudrate(uint32_t serial_baudrate) { this->M5_cfg_.serial_baudrate = serial_baudrate; }
void set_clear_display(bool clear_display) { this->M5_cfg_.clear_display = clear_display; }
void set_output_power(bool output_power) { this->M5_cfg_.output_power = output_power; }
void set_internal_imu(bool internal_imu) { this->M5_cfg_.internal_imu = internal_imu; }
void set_internal_rtc(bool internal_rtc) { this->M5_cfg_.internal_rtc = internal_rtc; }
void set_internal_spk(bool internal_spk) { this->M5_cfg_.internal_spk = internal_spk; }
void set_internal_mic(bool internal_mic) { this->M5_cfg_.internal_mic = internal_mic; }
void set_external_imu(bool external_imu) { this->M5_cfg_.external_imu = external_imu; }
void set_external_rtc(bool external_rtc) { this->M5_cfg_.external_rtc = external_rtc; }
void set_external_spk(bool external_spk) { this->M5_cfg_.external_spk = external_spk; }
void set_led_brightness(uint8_t led_brightness) { this->M5_cfg_.led_brightness = led_brightness; }
std::string get_board_name();
std::string get_imu_type();
std::string get_power_type();
int32_t get_battery_level() { return M5.Power.getBatteryLevel(); }
float get_temperature() {
M5.Imu.getTemp(&(this->imu_temperature_));
return this->imu_temperature_;
}
void set_battery_level_sensor(sensor::Sensor *sensor) { battery_level_sensor_ = sensor; }
void set_temperature_sensor(sensor::Sensor *sensor) { temperature_sensor_ = sensor; }
void set_battery_level_update_interval(uint32_t update_interval) {
this->set_interval("battery_level_update", update_interval * 1000,
[this] { this->battery_level_sensor_->publish_state(this->get_battery_level()); });
}
void set_temperature_update_interval(uint32_t update_interval) {
this->set_interval("temperature_update", update_interval * 1000,
[this] { this->temperature_sensor_->publish_state(this->get_temperature()); });
}
protected:
m5::M5Unified::config_t M5_cfg_{M5.config()};
sensor::Sensor *battery_level_sensor_;
sensor::Sensor *temperature_sensor_;
float imu_temperature_;
void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false);
};
together with an init.py to setup the yaml config for usage like this:
m5unified:
id: pion_m5unified
internal_spk: true
No need for GPIO pins or buses, as it is all done automagically
I have also successfully added a few sensors via sensor.py with a lot of trial and error that allows me to to define them like this:
sensor:
- platform: m5unified
battery_level:
name: ${device} Battery Level
id: pion_batterylevel
update_interval: 60s
So far, so good. It gets tricky for me with binary_sensors and later with the display.
I need a custom binary sensor class with a loop() function to map all the various supported buttons (Button A, B, C, Restart, Power, Touch, …). Listening to the events in the loop() is easy, but I am struggling to define the proper class and binary_sensor.py
Here is the skeleton for the buttons/binary_sensor class:
class M5UnifiedBinarySensor : public Component, public BinarySensor {
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void loop() override { delay(1); ... this is the easy part ...}
protected:
};
And here is the binary_sensor.py that should go with it:
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import (
DEVICE_CLASS_SWITCH,
STATE_CLASS_NONE,
)
from . import m5unified_ns, M5Unified, CONF_M5UNIFIED_ID
CODEOWNERS = ["@fxstein"]
DEPENDENCIES = ["m5unified"]
BUTTON_TYPES = [
"btn_A",
"btn_B",
"btn_C",
]
M5UnifiedBinarySensor = m5unified_ns.class_(
"M5UnifiedBinarySensor", binary_sensor.BinarySensor
)
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(M5UnifiedBinarySensor).extend(
{
cv.GenerateID(CONF_M5UNIFIED_ID): cv.use_id(M5Unified),
cv.Optional("btn_A"): M5UnifiedBinarySensor.sensor_schema(),
}
)
async def setup_conf(config, key, hub):
if key in config:
conf = config[key]
sens = await M5UnifiedBinarySensor.new_sensor(conf)
async def to_code(config):
hub = await cg.get_variable(config[CONF_M5UNIFIED_ID])
for key in BUTTON_TYPES:
await setup_conf(config, key, hub)
The C++ code is the easy part for me, but I have not had a chance to fully grasp the configuration validation and code generation aspect of the platform. I got the other pieces working by looking at various components, but now I am stuck.
Any help would be much appreciated!