Need help with Custom Component I am developing for M5Stack devices: M5Unified

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!

1 Like

And here is the example yaml for the binary_sensor button (used to do this via GPIO pins before but was limited to a few physical buttons)

binary_sensor:
  - platform: m5unified
    btn_A:
      id: pion_center_button
      on_press:
        then:
          - script.stop: pion_delayed_display_shut_down
          - display.page.show_next: pion_display
          - component.update: pion_display
          - script.execute: pion_display_full_brightness
      on_release:
        then:
          - script.execute: pion_delayed_display_shut_down

btn_A is just a placeholder while I do trial and error. Plan to map all the officially supported buttons into an enum in the config.

And here is the error I am getting:

Failed config

binary_sensor.m5unified: [source devices/m5stack-core2/sensors.yaml:44]
  platform: m5unified
  
  not a valid value.
  btn_A: 
    id: pion_center_button
    on_press: 
      then: 
        - script.stop: pion_delayed_display_shut_down
        - display.page.show_next: pion_display
        - component.update: pion_display
        - script.execute: pion_display_full_brightness
    on_release: 
      then: 
        - script.execute: pion_delayed_display_shut_down

One more bump to see if anyone is able to provide some pointers for me. Many thanks in advance!

Hi @fxstein, I’m curious if you’ve made any more headway with this. I just picked up an M5Stack Tough and am struggling to set it up. I just opened up a feature request on the esphome repo for official support: Support for M5Stack Tough ESP32 IoT Development Board Kit · Issue #2166 · esphome/feature-requests · GitHub

I have a minimum version that is working for me for now. Never got any of the devs to respond unfortunately.

If you want any help testing out the M5Stack Tough, I am happy to give it a go. M5Stack produces a bunch of nice units, but the out-of-the-box support is limited in ESPHome. I wish your project would get some traction with the devs.

1 Like

Hi, i ordered some Parts from M5Stack too and I think your project is very helpful for the community. Do you have a git repo? Here is mine: lubeda (LuBeDa) · GitHub

1 Like

Yes made some more progress but had to put it on the back burner as I had to focus on a custom KNX heating and solar integration. Should be back on the M5 devices within 2 weeks.

4 Likes

following too, could you please consider integrating also m5stack Station rs485?