mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	feat: add AS5600 component/sensor (#5174)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
							
								
								
									
										228
									
								
								esphome/components/as5600/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								esphome/components/as5600/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||
							
								
								
									
										138
									
								
								esphome/components/as5600/as5600.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								esphome/components/as5600/as5600.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<AS5600MagnetStatus>(status); | ||||
| } | ||||
|  | ||||
| optional<uint16_t> AS5600Component::read_position() { | ||||
|   uint16_t pos = 0; | ||||
|   if (!this->read_byte_16(REGISTER_ANGLE, &pos)) { | ||||
|     return {}; | ||||
|   } | ||||
|   return pos; | ||||
| } | ||||
|  | ||||
| optional<uint16_t> AS5600Component::read_raw_position() { | ||||
|   uint16_t pos = 0; | ||||
|   if (!this->read_byte_16(REGISTER_ANGLE_RAW, &pos)) { | ||||
|     return {}; | ||||
|   } | ||||
|   return pos; | ||||
| } | ||||
|  | ||||
| }  // namespace as5600 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										105
									
								
								esphome/components/as5600/as5600.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								esphome/components/as5600/as5600.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<uint16_t> read_position(); | ||||
|   optional<uint16_t> 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 | ||||
							
								
								
									
										119
									
								
								esphome/components/as5600/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								esphome/components/as5600/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||
							
								
								
									
										98
									
								
								esphome/components/as5600/sensor/as5600_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								esphome/components/as5600/sensor/as5600_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										43
									
								
								esphome/components/as5600/sensor/as5600_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/as5600/sensor/as5600_sensor.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<AS5600Component>, 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 | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user