From 991880d53fa2d21512dfeaf84ee1380a4bd32bbe Mon Sep 17 00:00:00 2001 From: Branden Cash <203336+ammmze@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:07:40 -0700 Subject: [PATCH] feat: add AS5600 component/sensor (#5174) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 + esphome/components/as5600/__init__.py | 228 ++++++++++++++++++ esphome/components/as5600/as5600.cpp | 138 +++++++++++ esphome/components/as5600/as5600.h | 105 ++++++++ esphome/components/as5600/sensor/__init__.py | 119 +++++++++ .../as5600/sensor/as5600_sensor.cpp | 98 ++++++++ .../components/as5600/sensor/as5600_sensor.h | 43 ++++ tests/test1.yaml | 22 ++ 8 files changed, 755 insertions(+) create mode 100644 esphome/components/as5600/__init__.py create mode 100644 esphome/components/as5600/as5600.cpp create mode 100644 esphome/components/as5600/as5600.h create mode 100644 esphome/components/as5600/sensor/__init__.py create mode 100644 esphome/components/as5600/sensor/as5600_sensor.cpp create mode 100644 esphome/components/as5600/sensor/as5600_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 320a23ffaa..fc14b2088e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -34,6 +34,8 @@ esphome/components/analog_threshold/* @ianchi esphome/components/animation/* @syndlex esphome/components/anova/* @buxtronix esphome/components/api/* @OttoWinter +esphome/components/as5600/* @ammmze +esphome/components/as5600/sensor/* @ammmze esphome/components/as7341/* @mrgnr esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py new file mode 100644 index 0000000000..1840b22768 --- /dev/null +++ b/esphome/components/as5600/__init__.py @@ -0,0 +1,228 @@ +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_DIR_PIN, + CONF_DIRECTION, + CONF_HYSTERESIS, + CONF_RANGE, +) + +CODEOWNERS = ["@ammmze"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +as5600_ns = cg.esphome_ns.namespace("as5600") +AS5600Component = as5600_ns.class_("AS5600Component", cg.Component, i2c.I2CDevice) + +DIRECTION = { + "CLOCKWISE": 0, + "COUNTERCLOCKWISE": 1, +} + +POWER_MODE = { + "NOMINAL": 0, + "LOW1": 1, + "LOW2": 2, + "LOW3": 3, +} + +HYSTERESIS = { + "NONE": 0, + "LSB1": 1, + "LSB2": 2, + "LSB3": 3, +} + +SLOW_FILTER = { + "16X": 0, + "8X": 1, + "4X": 2, + "2X": 3, +} + +FAST_FILTER = { + "NONE": 0, + "LSB6": 1, + "LSB7": 2, + "LSB9": 3, + "LSB18": 4, + "LSB21": 5, + "LSB24": 6, + "LSB10": 7, +} + +CONF_ANGLE = "angle" +CONF_RAW_ANGLE = "raw_angle" +CONF_RAW_POSITION = "raw_position" +CONF_WATCHDOG = "watchdog" +CONF_POWER_MODE = "power_mode" +CONF_SLOW_FILTER = "slow_filter" +CONF_FAST_FILTER = "fast_filter" +CONF_START_POSITION = "start_position" +CONF_END_POSITION = "end_position" + + +RESOLUTION = 4096 +MAX_POSITION = RESOLUTION - 1 +ANGLE_TO_POSITION = RESOLUTION / 360 +POSITION_TO_ANGLE = 360 / RESOLUTION +# validate min range of 18deg (per datasheet) ... though i seem to get valid values down to a range of 192steps (16.875deg) +MIN_RANGE = round(18 * ANGLE_TO_POSITION) + + +def angle(min=-360, max=360): + return cv.All( + cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max) + ) + + +def angle_to_position(value, min=-360, max=360): + try: + value = angle(min=min, max=max)(value) + return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION + except cv.Invalid as e: + raise cv.Invalid(f"When using angle, {e.error_message}") + + +def percent_to_position(value): + value = cv.possibly_negative_percentage(value) + return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION + + +def position(min=-MAX_POSITION, max=MAX_POSITION): + """Validate that the config option is a position. + Accepts integers, degrees, or percentage (of 360 degrees). + """ + + def validator(value): + if isinstance(value, str) and value.endswith("%"): + value = percent_to_position(value) + + if isinstance(value, str) and (value.endswith("°") or value.endswith("deg")): + return angle_to_position( + value, + min=round(min * POSITION_TO_ANGLE), + max=round(max * POSITION_TO_ANGLE), + ) + + return cv.int_range(min=min, max=max)(value) + + return validator + + +def position_range(): + """Validate that value given is a valid range for the device. + A valid range is one of the following: + - a value of 0 (meaning full range) + - 18 thru 360 degrees + - negative 360 thru negative 18 degrees (notes: these are normalized to their positive values, accepting negatives is for convenience) + """ + zero_validator = position(min=0, max=0) + negative_validator = cv.Any( + position(min=-MAX_POSITION, max=-MIN_RANGE), + zero_validator, + ) + positive_validator = cv.Any( + position(min=MIN_RANGE, max=MAX_POSITION), + zero_validator, + ) + + def validator(value): + is_negative_str = isinstance(value, str) and value.startswith("-") + is_negative_num = isinstance(value, (float, int)) and value < 0 + if is_negative_str or is_negative_num: + return negative_validator(value) + return positive_validator(value) + + return validator + + +def has_valid_range_config(): + """Validate that that the config start + end position results in a valid + positional range, which must be >= 18degrees + """ + range_validator = position_range() + + def validator(config): + # if we don't have an end position, then there is nothing to do + if CONF_END_POSITION not in config: + return config + + # determine the range by taking the difference from the end and start + range = config[CONF_END_POSITION] - config[CONF_START_POSITION] + + # but need to account for start position being greater than end position + # where the range rolls back around the 0 position + if config[CONF_END_POSITION] < config[CONF_START_POSITION]: + range = RESOLUTION + config[CONF_END_POSITION] - config[CONF_START_POSITION] + + try: + range_validator(range) + return config + except cv.Invalid as e: + raise cv.Invalid( + f"The range between start and end position is invalid. It was was {range} but {e.error_message}" + ) + + return validator + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(AS5600Component), + cv.Optional(CONF_DIR_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_DIRECTION, default="CLOCKWISE"): cv.enum( + DIRECTION, upper=True + ), + cv.Optional(CONF_WATCHDOG, default=False): cv.boolean, + cv.Optional(CONF_POWER_MODE, default="NOMINAL"): cv.enum( + POWER_MODE, upper=True, space="" + ), + cv.Optional(CONF_HYSTERESIS, default="NONE"): cv.enum( + HYSTERESIS, upper=True, space="" + ), + cv.Optional(CONF_SLOW_FILTER, default="16X"): cv.enum( + SLOW_FILTER, upper=True, space="" + ), + cv.Optional(CONF_FAST_FILTER, default="NONE"): cv.enum( + FAST_FILTER, upper=True, space="" + ), + cv.Optional(CONF_START_POSITION, default=0): position(), + cv.Optional(CONF_END_POSITION): position(), + cv.Optional(CONF_RANGE): position_range(), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x36)), + # ensure end_position and range are mutually exclusive + cv.has_at_most_one_key(CONF_END_POSITION, CONF_RANGE), + has_valid_range_config(), +) + + +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) + + cg.add(var.set_direction(config[CONF_DIRECTION])) + cg.add(var.set_watchdog(config[CONF_WATCHDOG])) + cg.add(var.set_power_mode(config[CONF_POWER_MODE])) + cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) + cg.add(var.set_slow_filter(config[CONF_SLOW_FILTER])) + cg.add(var.set_fast_filter(config[CONF_FAST_FILTER])) + cg.add(var.set_start_position(config[CONF_START_POSITION])) + + if dir_pin_config := config.get(CONF_DIR_PIN): + pin = await cg.gpio_pin_expression(dir_pin_config) + cg.add(var.set_dir_pin(pin)) + + if (end_position_config := config.get(CONF_END_POSITION, None)) is not None: + cg.add(var.set_end_position(end_position_config)) + + if (range_config := config.get(CONF_RANGE, None)) is not None: + cg.add(var.set_range(range_config)) diff --git a/esphome/components/as5600/as5600.cpp b/esphome/components/as5600/as5600.cpp new file mode 100644 index 0000000000..3fe7eab58d --- /dev/null +++ b/esphome/components/as5600/as5600.cpp @@ -0,0 +1,138 @@ +#include "as5600.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace as5600 { + +static const char *const TAG = "as5600"; + +// Configuration registers +static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R +static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW +static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW +static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW +static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW + +// Output registers +static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R +static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R + +// Status registers +static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R +static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R +static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R + +void AS5600Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up AS5600..."); + + if (!this->read_byte(REGISTER_STATUS).has_value()) { + this->mark_failed(); + return; + } + + // configuration direction pin, if given + // the dir pin on the chip should be low for clockwise + // and high for counterclockwise. If the pin is left floating + // the reported positions will be erratic. + if (this->dir_pin_ != nullptr) { + this->dir_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->dir_pin_->digital_write(this->direction_ == 1); + } + + // build config register + // take the value, shift it left, and add mask to it to ensure we + // are only changing the bits appropriate for that setting in the + // off chance we somehow have bad value in there and it makes for + // a nice visual for the bit positions. + uint16_t config = 0; + // clang-format off + config |= (this->watchdog_ << 13) & 0b0010000000000000; + config |= (this->fast_filter_ << 10) & 0b0001110000000000; + config |= (this->slow_filter_ << 8) & 0b0000001100000000; + config |= (this->pwm_frequency_ << 6) & 0b0000000011000000; + config |= (this->output_mode_ << 4) & 0b0000000000110000; + config |= (this->hysteresis_ << 2) & 0b0000000000001100; + config |= (this->power_mode_ << 0) & 0b0000000000000011; + // clang-format on + + // write config to config register + if (!this->write_byte_16(REGISTER_CONF, config)) { + this->mark_failed(); + return; + } + + // configure the start position + this->write_byte_16(REGISTER_ZPOS, this->start_position_); + + // configure either end position or max angle + if (this->end_mode_ == END_MODE_POSITION) { + this->write_byte_16(REGISTER_MPOS, this->end_position_); + } else { + this->write_byte_16(REGISTER_MANG, this->end_position_); + } + + // calculate the raw max from end position or start + range + this->raw_max_ = this->end_mode_ == END_MODE_POSITION ? this->end_position_ & 4095 + : (this->start_position_ + this->end_position_) & 4095; + + // calculate allowed range of motion by taking the start from the end + // but only if the end is greater than the start. If the start is greater + // than the end position, then that means we take the start all the way to + // reset point (i.e. 0 deg raw) and then we that with the end position + uint16_t range = this->raw_max_ > this->start_position_ ? this->raw_max_ - this->start_position_ + : (4095 - this->start_position_) + this->raw_max_; + + // range scale is ratio of actual allowed range to the full range + this->range_scale_ = range / 4095.0f; +} + +void AS5600Component::dump_config() { + ESP_LOGCONFIG(TAG, "AS5600:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with AS5600 failed!"); + return; + } + + ESP_LOGCONFIG(TAG, " Watchdog: %d", this->watchdog_); + ESP_LOGCONFIG(TAG, " Fast Filter: %d", this->fast_filter_); + ESP_LOGCONFIG(TAG, " Slow Filter: %d", this->slow_filter_); + ESP_LOGCONFIG(TAG, " Hysteresis: %d", this->hysteresis_); + ESP_LOGCONFIG(TAG, " Start Position: %d", this->start_position_); + if (this->end_mode_ == END_MODE_POSITION) { + ESP_LOGCONFIG(TAG, " End Position: %d", this->end_position_); + } else { + ESP_LOGCONFIG(TAG, " Range: %d", this->end_position_); + } +} + +bool AS5600Component::in_range(uint16_t raw_position) { + return this->raw_max_ > this->start_position_ + ? raw_position >= this->start_position_ && raw_position <= this->raw_max_ + : raw_position >= this->start_position_ || raw_position <= this->raw_max_; +} + +AS5600MagnetStatus AS5600Component::read_magnet_status() { + uint8_t status = this->reg(REGISTER_STATUS).get() >> 3 & 0b000111; + return static_cast(status); +} + +optional AS5600Component::read_position() { + uint16_t pos = 0; + if (!this->read_byte_16(REGISTER_ANGLE, &pos)) { + return {}; + } + return pos; +} + +optional AS5600Component::read_raw_position() { + uint16_t pos = 0; + if (!this->read_byte_16(REGISTER_ANGLE_RAW, &pos)) { + return {}; + } + return pos; +} + +} // namespace as5600 +} // namespace esphome diff --git a/esphome/components/as5600/as5600.h b/esphome/components/as5600/as5600.h new file mode 100644 index 0000000000..fbfd18db40 --- /dev/null +++ b/esphome/components/as5600/as5600.h @@ -0,0 +1,105 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/preferences.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace as5600 { + +static const uint16_t POSITION_COUNT = 4096; +static const float RAW_TO_DEGREES = 360.0 / POSITION_COUNT; +static const float DEGREES_TO_RAW = POSITION_COUNT / 360.0; + +enum EndPositionMode : uint8_t { + // In this mode, the end position is calculated by taking the start position + // and adding the range/positions. For example, you could say start at 90deg, + // and have a range of 180deg and effectively the sensor will report values + // from the physical 90deg thru 270deg. + END_MODE_RANGE, + // In this mode, the end position is explicitly set, and changing the start + // position will NOT change the end position. + END_MODE_POSITION, +}; + +enum OutRangeMode : uint8_t { + // In this mode, the AS5600 chip itself actually reports these values, but + // effectively it splits the out-of-range values in half, and when positioned + // over the half closest to the min/start position, it will report 0 and when + // positioned over the half closes to the max/end position, it will report the + // max/end value. + OUT_RANGE_MODE_MIN_MAX, + // In this mode, when the magnet is positioned outside the configured + // range, the sensor will report NAN, which translates to "Unknown" + // in Home Assistant. + OUT_RANGE_MODE_NAN, +}; + +enum AS5600MagnetStatus : uint8_t { + MAGNET_GONE = 2, // 0b010 / magnet not detected + MAGNET_OK = 4, // 0b100 / magnet just right + MAGNET_STRONG = 5, // 0b101 / magnet too strong + MAGNET_WEAK = 6, // 0b110 / magnet too weak +}; + +class AS5600Component : public Component, public i2c::I2CDevice { + public: + /// Set up the internal sensor array. + void setup() override; + void dump_config() override; + /// HARDWARE_LATE setup priority + float get_setup_priority() const override { return setup_priority::DATA; } + + // configuration setters + void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; } + void set_direction(uint8_t direction) { this->direction_ = direction; } + void set_fast_filter(uint8_t fast_filter) { this->fast_filter_ = fast_filter; } + void set_hysteresis(uint8_t hysteresis) { this->hysteresis_ = hysteresis; } + void set_power_mode(uint8_t power_mode) { this->power_mode_ = power_mode; } + void set_slow_filter(uint8_t slow_filter) { this->slow_filter_ = slow_filter; } + void set_watchdog(bool watchdog) { this->watchdog_ = watchdog; } + bool get_watchdog() { return this->watchdog_; } + void set_start_position(uint16_t start_position) { this->start_position_ = start_position % POSITION_COUNT; } + void set_end_position(uint16_t end_position) { + this->end_position_ = end_position % POSITION_COUNT; + this->end_mode_ = END_MODE_POSITION; + } + void set_range(uint16_t range) { + this->end_position_ = range % POSITION_COUNT; + this->end_mode_ = END_MODE_RANGE; + } + + // Gets the scale value for the configured range. + // For example, if configured to start at 0deg and end at 180deg, the + // range is 50% of the native/raw range, so the range scale would be 0.5. + // If configured to use the full 360deg, the range scale would be 1.0. + float get_range_scale() { return this->range_scale_; } + + // Indicates whether the given *raw* position is within the configured range + bool in_range(uint16_t raw_position); + + AS5600MagnetStatus read_magnet_status(); + optional read_position(); + optional read_raw_position(); + + protected: + InternalGPIOPin *dir_pin_{nullptr}; + uint8_t direction_; + uint8_t fast_filter_; + uint8_t hysteresis_; + uint8_t power_mode_; + uint8_t slow_filter_; + uint8_t pwm_frequency_{0}; + uint8_t output_mode_{0}; + bool watchdog_; + uint16_t start_position_; + uint16_t end_position_{0}; + uint16_t raw_max_; + EndPositionMode end_mode_{END_MODE_RANGE}; + float range_scale_{1.0}; +}; + +} // namespace as5600 +} // namespace esphome diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py new file mode 100644 index 0000000000..589a66950a --- /dev/null +++ b/esphome/components/as5600/sensor/__init__.py @@ -0,0 +1,119 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + STATE_CLASS_MEASUREMENT, + ICON_MAGNET, + ICON_ROTATE_RIGHT, + CONF_GAIN, + ENTITY_CATEGORY_DIAGNOSTIC, + CONF_MAGNITUDE, + CONF_STATUS, + CONF_POSITION, +) +from .. import as5600_ns, AS5600Component + +CODEOWNERS = ["@ammmze"] +DEPENDENCIES = ["as5600"] + +AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent) + +CONF_ANGLE = "angle" +CONF_RAW_ANGLE = "raw_angle" +CONF_RAW_POSITION = "raw_position" +CONF_WATCHDOG = "watchdog" +CONF_POWER_MODE = "power_mode" +CONF_SLOW_FILTER = "slow_filter" +CONF_FAST_FILTER = "fast_filter" +CONF_PWM_FREQUENCY = "pwm_frequency" +CONF_BURN_COUNT = "burn_count" +CONF_START_POSITION = "start_position" +CONF_END_POSITION = "end_position" +CONF_OUT_OF_RANGE_MODE = "out_of_range_mode" + +OutOfRangeMode = as5600_ns.enum("OutRangeMode") +OUT_OF_RANGE_MODES = { + "MIN_MAX": OutOfRangeMode.OUT_RANGE_MODE_MIN_MAX, + "NAN": OutOfRangeMode.OUT_RANGE_MODE_NAN, +} + + +CONF_AS5600_ID = "as5600_id" +CONFIG_SCHEMA = ( + sensor.sensor_schema( + AS5600Sensor, + accuracy_decimals=0, + icon=ICON_ROTATE_RIGHT, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_AS5600_ID): cv.use_id(AS5600Component), + cv.Optional(CONF_OUT_OF_RANGE_MODE): cv.enum( + OUT_OF_RANGE_MODES, upper=True, space="_" + ), + cv.Optional(CONF_RAW_POSITION): sensor.sensor_schema( + accuracy_decimals=0, + icon=ICON_ROTATE_RIGHT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_GAIN): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_MAGNITUDE): sensor.sensor_schema( + accuracy_decimals=0, + icon=ICON_MAGNET, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_STATUS): sensor.sensor_schema( + accuracy_decimals=0, + icon=ICON_MAGNET, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_AS5600_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE): + cg.add(var.set_out_of_range_mode(out_of_range_mode_config)) + + if angle_config := config.get(CONF_ANGLE): + sens = await sensor.new_sensor(angle_config) + cg.add(var.set_angle_sensor(sens)) + + if raw_angle_config := config.get(CONF_RAW_ANGLE): + sens = await sensor.new_sensor(raw_angle_config) + cg.add(var.set_raw_angle_sensor(sens)) + + if position_config := config.get(CONF_POSITION): + sens = await sensor.new_sensor(position_config) + cg.add(var.set_position_sensor(sens)) + + if raw_position_config := config.get(CONF_RAW_POSITION): + sens = await sensor.new_sensor(raw_position_config) + cg.add(var.set_raw_position_sensor(sens)) + + if gain_config := config.get(CONF_GAIN): + sens = await sensor.new_sensor(gain_config) + cg.add(var.set_gain_sensor(sens)) + + if magnitude_config := config.get(CONF_MAGNITUDE): + sens = await sensor.new_sensor(magnitude_config) + cg.add(var.set_magnitude_sensor(sens)) + + if status_config := config.get(CONF_STATUS): + sens = await sensor.new_sensor(status_config) + cg.add(var.set_status_sensor(sens)) diff --git a/esphome/components/as5600/sensor/as5600_sensor.cpp b/esphome/components/as5600/sensor/as5600_sensor.cpp new file mode 100644 index 0000000000..feb8f6cebf --- /dev/null +++ b/esphome/components/as5600/sensor/as5600_sensor.cpp @@ -0,0 +1,98 @@ +#include "as5600_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace as5600 { + +static const char *const TAG = "as5600.sensor"; + +// Configuration registers +static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R +static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW +static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW +static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW +static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW + +// Output registers +static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R +static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R + +// Status registers +static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R +static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R +static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R + +float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void AS5600Sensor::dump_config() { + LOG_SENSOR("", "AS5600 Sensor", this); + ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_); + if (this->angle_sensor_ != nullptr) { + LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_); + } + if (this->raw_angle_sensor_ != nullptr) { + LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_); + } + if (this->position_sensor_ != nullptr) { + LOG_SENSOR(" ", "Position Sensor", this->position_sensor_); + } + if (this->raw_position_sensor_ != nullptr) { + LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_); + } + if (this->gain_sensor_ != nullptr) { + LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_); + } + if (this->magnitude_sensor_ != nullptr) { + LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_); + } + if (this->status_sensor_ != nullptr) { + LOG_SENSOR(" ", "Status Sensor", this->status_sensor_); + } + LOG_UPDATE_INTERVAL(this); +} + +void AS5600Sensor::update() { + if (this->gain_sensor_ != nullptr) { + this->gain_sensor_->publish_state(this->parent_->reg(REGISTER_AGC).get()); + } + + if (this->magnitude_sensor_ != nullptr) { + uint16_t value = 0; + this->parent_->read_byte_16(REGISTER_MAGNITUDE, &value); + this->magnitude_sensor_->publish_state(value); + } + + // 2 = magnet not detected + // 4 = magnet just right + // 5 = magnet too strong + // 6 = magnet too weak + if (this->status_sensor_ != nullptr) { + this->status_sensor_->publish_state(this->parent_->read_magnet_status()); + } + + auto pos = this->parent_->read_position(); + if (!pos.has_value()) { + this->status_set_warning(); + return; + } + + auto raw = this->parent_->read_raw_position(); + if (!raw.has_value()) { + this->status_set_warning(); + return; + } + + if (this->out_of_range_mode_ == OUT_RANGE_MODE_NAN) { + this->publish_state(this->parent_->in_range(raw.value()) ? pos.value() : NAN); + } else { + this->publish_state(pos.value()); + } + + if (this->raw_position_sensor_ != nullptr) { + this->raw_position_sensor_->publish_state(raw.value()); + } + this->status_clear_warning(); +} + +} // namespace as5600 +} // namespace esphome diff --git a/esphome/components/as5600/sensor/as5600_sensor.h b/esphome/components/as5600/sensor/as5600_sensor.h new file mode 100644 index 0000000000..0af9b01ae5 --- /dev/null +++ b/esphome/components/as5600/sensor/as5600_sensor.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/preferences.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/as5600/as5600.h" + +namespace esphome { +namespace as5600 { + +class AS5600Sensor : public PollingComponent, public Parented, public sensor::Sensor { + public: + void update() override; + void dump_config() override; + float get_setup_priority() const override; + + void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; } + void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; } + void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; } + void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) { + this->raw_position_sensor_ = raw_position_sensor; + } + void set_gain_sensor(sensor::Sensor *gain_sensor) { this->gain_sensor_ = gain_sensor; } + void set_magnitude_sensor(sensor::Sensor *magnitude_sensor) { this->magnitude_sensor_ = magnitude_sensor; } + void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; } + void set_out_of_range_mode(OutRangeMode oor_mode) { this->out_of_range_mode_ = oor_mode; } + OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; } + + protected: + sensor::Sensor *angle_sensor_{nullptr}; + sensor::Sensor *raw_angle_sensor_{nullptr}; + sensor::Sensor *position_sensor_{nullptr}; + sensor::Sensor *raw_position_sensor_{nullptr}; + sensor::Sensor *gain_sensor_{nullptr}; + sensor::Sensor *magnitude_sensor_{nullptr}; + sensor::Sensor *status_sensor_{nullptr}; + OutRangeMode out_of_range_mode_{OUT_RANGE_MODE_MIN_MAX}; +}; + +} // namespace as5600 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index aaad10ff0a..2748a09c59 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -322,6 +322,18 @@ ads1115: address: 0x48 i2c_id: i2c_bus +as5600: + i2c_id: i2c_bus + dir_pin: GPIO27 + direction: clockwise + start_position: 90deg + range: 180deg + watchdog: true + power_mode: low1 + hysteresis: lsb1 + slow_filter: 8x + fast_filter: lsb6 + dallas: pin: allow_other_uses: true @@ -555,6 +567,16 @@ sensor: state_topic: hi/me retain: false availability: + - platform: as5600 + name: AS5600 Position + raw_position: + name: AS5600 Raw Position + gain: + name: AS5600 Gain + magnitude: + name: AS5600 Magnitude + status: + name: AS5600 Status - platform: as7341 update_interval: 15s gain: X8