Feature/m5angle8: Add support for m5angle8 input device (#6799)

Co-authored-by: Richard Nauber <richard@nauber.dev>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
rnauber 2024-07-21 23:57:59 +02:00 committed by GitHub
parent 368662969e
commit 40e79299d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 474 additions and 0 deletions

View file

@ -216,6 +216,7 @@ esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core esphome/components/logger/* @esphome/core
esphome/components/ltr390/* @latonita @sjtrny esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr_als_ps/* @latonita esphome/components/ltr_als_ps/* @latonita
esphome/components/m5stack_8angle/* @rnauber
esphome/components/matrix_keypad/* @ssieb esphome/components/matrix_keypad/* @ssieb
esphome/components/max31865/* @DAVe3283 esphome/components/max31865/* @DAVe3283
esphome/components/max44009/* @berfenger esphome/components/max44009/* @berfenger

View file

@ -0,0 +1,33 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import CONF_ID
DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@rnauber"]
MULTI_CONF = True
CONF_M5STACK_8ANGLE_ID = "m5stack_8angle_id"
m5stack_8angle_ns = cg.esphome_ns.namespace("m5stack_8angle")
M5Stack8AngleComponent = m5stack_8angle_ns.class_(
"M5Stack8AngleComponent",
i2c.I2CDevice,
cg.Component,
)
AnalogBits = m5stack_8angle_ns.enum("AnalogBits")
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(M5Stack8AngleComponent),
}
).extend(i2c.i2c_device_schema(0x43))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View file

@ -0,0 +1,30 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID
M5Stack8AngleSwitchBinarySensor = m5stack_8angle_ns.class_(
"M5Stack8AngleSwitchBinarySensor",
binary_sensor.BinarySensor,
cg.PollingComponent,
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
}
)
.extend(binary_sensor.binary_sensor_schema(M5Stack8AngleSwitchBinarySensor))
.extend(cv.polling_component_schema("10s"))
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID])
sens = await binary_sensor.new_binary_sensor(config)
cg.add(sens.set_parent(hub))
await cg.register_component(sens, config)

View file

@ -0,0 +1,17 @@
#include "m5stack_8angle_binary_sensor.h"
namespace esphome {
namespace m5stack_8angle {
void M5Stack8AngleSwitchBinarySensor::update() {
int8_t out = this->parent_->read_switch();
if (out == -1) {
this->status_set_warning("Could not read binary sensor state from M5Stack 8Angle.");
return;
}
this->publish_state(out != 0);
this->status_clear_warning();
}
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,19 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/core/component.h"
#include "../m5stack_8angle.h"
namespace esphome {
namespace m5stack_8angle {
class M5Stack8AngleSwitchBinarySensor : public binary_sensor::BinarySensor,
public PollingComponent,
public Parented<M5Stack8AngleComponent> {
public:
void update() override;
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,31 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import light
from esphome.const import CONF_OUTPUT_ID
from .. import M5Stack8AngleComponent, m5stack_8angle_ns, CONF_M5STACK_8ANGLE_ID
M5Stack8AngleLightsComponent = m5stack_8angle_ns.class_(
"M5Stack8AngleLightOutput",
light.AddressableLight,
)
CONFIG_SCHEMA = cv.All(
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
{
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(M5Stack8AngleLightsComponent),
}
)
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_M5STACK_8ANGLE_ID])
lights = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await light.register_light(lights, config)
await cg.register_component(lights, config)
cg.add(lights.set_parent(hub))

View file

@ -0,0 +1,45 @@
#include "m5stack_8angle_light.h"
#include "esphome/core/log.h"
namespace esphome {
namespace m5stack_8angle {
static const char *const TAG = "m5stack_8angle.light";
void M5Stack8AngleLightOutput::setup() {
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->buf_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
if (this->buf_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate buffer of size %u", M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
this->mark_failed();
return;
};
memset(this->buf_, 0xFF, M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED);
this->effect_data_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS);
if (this->effect_data_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate effect data of size %u", M5STACK_8ANGLE_NUM_LEDS);
this->mark_failed();
return;
};
memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS);
}
void M5Stack8AngleLightOutput::write_state(light::LightState *state) {
for (int i = 0; i < M5STACK_8ANGLE_NUM_LEDS;
i++) { // write one LED at a time, otherwise the message will be truncated
this->parent_->write_register(M5STACK_8ANGLE_REGISTER_RGB_24B + i * M5STACK_8ANGLE_BYTES_PER_LED,
this->buf_ + i * M5STACK_8ANGLE_BYTES_PER_LED, M5STACK_8ANGLE_BYTES_PER_LED);
}
}
light::ESPColorView M5Stack8AngleLightOutput::get_view_internal(int32_t index) const {
size_t pos = index * M5STACK_8ANGLE_BYTES_PER_LED;
// red, green, blue, white, effect_data, color_correction
return {this->buf_ + pos, this->buf_ + pos + 1, this->buf_ + pos + 2,
nullptr, this->effect_data_ + index, &this->correction_};
}
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,37 @@
#pragma once
#include "esphome/components/light/addressable_light.h"
#include "esphome/components/light/light_output.h"
#include "../m5stack_8angle.h"
namespace esphome {
namespace m5stack_8angle {
static const uint8_t M5STACK_8ANGLE_NUM_LEDS = 9;
static const uint8_t M5STACK_8ANGLE_BYTES_PER_LED = 4;
class M5Stack8AngleLightOutput : public light::AddressableLight, public Parented<M5Stack8AngleComponent> {
public:
void setup() override;
void write_state(light::LightState *state) override;
int32_t size() const override { return M5STACK_8ANGLE_NUM_LEDS; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
traits.set_supported_color_modes({light::ColorMode::RGB});
return traits;
};
void clear_effect_data() override { memset(this->effect_data_, 0x00, M5STACK_8ANGLE_NUM_LEDS); };
protected:
light::ESPColorView get_view_internal(int32_t index) const override;
uint8_t *buf_{nullptr};
uint8_t *effect_data_{nullptr};
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,74 @@
#include "m5stack_8angle.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace m5stack_8angle {
static const char *const TAG = "m5stack_8angle";
void M5Stack8AngleComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up M5STACK_8ANGLE...");
i2c::ErrorCode err;
err = this->read(nullptr, 0);
if (err != i2c::NO_ERROR) {
ESP_LOGE(TAG, "I2C error %02X...", err);
this->mark_failed();
return;
};
err = this->read_register(M5STACK_8ANGLE_REGISTER_FW_VERSION, &this->fw_version_, 1);
if (err != i2c::NO_ERROR) {
ESP_LOGE(TAG, "I2C error %02X...", err);
this->mark_failed();
return;
};
}
void M5Stack8AngleComponent::dump_config() {
ESP_LOGCONFIG(TAG, "M5STACK_8ANGLE:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " Firmware version: %d ", this->fw_version_);
}
float M5Stack8AngleComponent::read_knob_pos(uint8_t channel, AnalogBits bits) {
int32_t raw_pos = this->read_knob_pos_raw(channel, bits);
if (raw_pos == -1) {
return NAN;
}
return (float) raw_pos / ((1 << bits) - 1);
}
int32_t M5Stack8AngleComponent::read_knob_pos_raw(uint8_t channel, AnalogBits bits) {
uint16_t knob_pos = 0;
i2c::ErrorCode err;
if (bits == BITS_8) {
err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B + channel, (uint8_t *) &knob_pos, 1);
} else if (bits == BITS_12) {
err = this->read_register(M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B + (channel * 2), (uint8_t *) &knob_pos, 2);
} else {
ESP_LOGE(TAG, "Invalid number of bits: %d", bits);
return -1;
}
if (err == i2c::NO_ERROR) {
return knob_pos;
} else {
return -1;
}
}
int8_t M5Stack8AngleComponent::read_switch() {
uint8_t out;
i2c::ErrorCode err = this->read_register(M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT, (uint8_t *) &out, 1);
if (err == i2c::NO_ERROR) {
return out ? 1 : 0;
} else {
return -1;
}
}
float M5Stack8AngleComponent::get_setup_priority() const { return setup_priority::DATA; }
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,34 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
namespace esphome {
namespace m5stack_8angle {
static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_12B = 0x00;
static const uint8_t M5STACK_8ANGLE_REGISTER_ANALOG_INPUT_8B = 0x10;
static const uint8_t M5STACK_8ANGLE_REGISTER_DIGITAL_INPUT = 0x20;
static const uint8_t M5STACK_8ANGLE_REGISTER_RGB_24B = 0x30;
static const uint8_t M5STACK_8ANGLE_REGISTER_FW_VERSION = 0xFE;
enum AnalogBits : uint8_t {
BITS_8 = 8,
BITS_12 = 12,
};
class M5Stack8AngleComponent : public i2c::I2CDevice, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
float read_knob_pos(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8);
int32_t read_knob_pos_raw(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8);
int8_t read_switch();
protected:
uint8_t fw_version_;
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,66 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_BIT_DEPTH,
CONF_CHANNEL,
CONF_RAW,
ICON_ROTATE_RIGHT,
STATE_CLASS_MEASUREMENT,
)
from .. import (
AnalogBits,
M5Stack8AngleComponent,
m5stack_8angle_ns,
CONF_M5STACK_8ANGLE_ID,
)
M5Stack8AngleKnobSensor = m5stack_8angle_ns.class_(
"M5Stack8AngleKnobSensor",
sensor.Sensor,
cg.PollingComponent,
)
BIT_DEPTHS = {
8: AnalogBits.BITS_8,
12: AnalogBits.BITS_12,
}
_validate_bits = cv.float_with_unit("bits", "bit")
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(M5Stack8AngleKnobSensor),
cv.GenerateID(CONF_M5STACK_8ANGLE_ID): cv.use_id(M5Stack8AngleComponent),
cv.Required(CONF_CHANNEL): cv.int_range(min=1, max=8),
cv.Optional(CONF_BIT_DEPTH, default="8bit"): cv.All(
_validate_bits, cv.enum(BIT_DEPTHS)
),
cv.Optional(CONF_RAW, default=False): cv.boolean,
}
)
.extend(
sensor.sensor_schema(
M5Stack8AngleKnobSensor,
accuracy_decimals=2,
icon=ICON_ROTATE_RIGHT,
state_class=STATE_CLASS_MEASUREMENT,
)
)
.extend(cv.polling_component_schema("10s"))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_M5STACK_8ANGLE_ID])
cg.add(var.set_channel(config[CONF_CHANNEL] - 1))
cg.add(var.set_bit_depth(BIT_DEPTHS[config[CONF_BIT_DEPTH]]))
cg.add(var.set_raw(config[CONF_RAW]))

View file

@ -0,0 +1,24 @@
#include "m5stack_8angle_sensor.h"
namespace esphome {
namespace m5stack_8angle {
void M5Stack8AngleKnobSensor::update() {
if (this->parent_ != nullptr) {
int32_t raw_pos = this->parent_->read_knob_pos_raw(this->channel_, this->bits_);
if (raw_pos == -1) {
this->status_set_warning("Could not read knob position from M5Stack 8Angle.");
return;
}
if (this->raw_) {
this->publish_state(raw_pos);
} else {
float knob_pos = (float) raw_pos / ((1 << this->bits_) - 1);
this->publish_state(knob_pos);
}
this->status_clear_warning();
};
}
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,27 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "../m5stack_8angle.h"
namespace esphome {
namespace m5stack_8angle {
class M5Stack8AngleKnobSensor : public sensor::Sensor,
public PollingComponent,
public Parented<M5Stack8AngleComponent> {
public:
void update() override;
void set_channel(uint8_t channel) { this->channel_ = channel; };
void set_bit_depth(AnalogBits bits) { this->bits_ = bits; };
void set_raw(bool raw) { this->raw_ = raw; };
protected:
uint8_t channel_;
AnalogBits bits_;
bool raw_;
};
} // namespace m5stack_8angle
} // namespace esphome

View file

@ -0,0 +1,30 @@
i2c:
sda: 0
scl: 1
id: bus_external
m5stack_8angle:
i2c_id: bus_external
id: m5stack_8angle_base
light:
- platform: m5stack_8angle
m5stack_8angle_id: m5stack_8angle_base
id: m8_angle_leds
name: Lights
effects:
- addressable_scan:
sensor:
- platform: m5stack_8angle
m5stack_8angle_id: m5stack_8angle_base
channel: 1
name: Knob 1
- platform: m5stack_8angle
m5stack_8angle_id: m5stack_8angle_base
channel: 2
name: Knob 2
binary_sensor:
- platform: m5stack_8angle
m5stack_8angle_id: m5stack_8angle_base
name: Switch

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml