mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Add support for PCA9535 16 bit I/O expander (#5634)
This commit is contained in:
		| @@ -217,7 +217,7 @@ esphome/components/number/* @esphome/core | |||||||
| esphome/components/ota/* @esphome/core | esphome/components/ota/* @esphome/core | ||||||
| esphome/components/output/* @esphome/core | esphome/components/output/* @esphome/core | ||||||
| esphome/components/pca6416a/* @Mat931 | esphome/components/pca6416a/* @Mat931 | ||||||
| esphome/components/pca9554/* @hwstar | esphome/components/pca9554/* @clydebarrow @hwstar | ||||||
| esphome/components/pcf85063/* @brogon | esphome/components/pcf85063/* @brogon | ||||||
| esphome/components/pcf8563/* @KoenBreeman | esphome/components/pcf8563/* @KoenBreeman | ||||||
| esphome/components/pid/* @OttoWinter | esphome/components/pid/* @OttoWinter | ||||||
|   | |||||||
| @@ -10,10 +10,12 @@ from esphome.const import ( | |||||||
|     CONF_INVERTED, |     CONF_INVERTED, | ||||||
|     CONF_OUTPUT, |     CONF_OUTPUT, | ||||||
| ) | ) | ||||||
|  | from esphome.core import CORE, ID | ||||||
|  |  | ||||||
| CODEOWNERS = ["@hwstar"] | CODEOWNERS = ["@hwstar", "@clydebarrow"] | ||||||
| DEPENDENCIES = ["i2c"] | DEPENDENCIES = ["i2c"] | ||||||
| MULTI_CONF = True | MULTI_CONF = True | ||||||
|  | CONF_PIN_COUNT = "pin_count" | ||||||
| pca9554_ns = cg.esphome_ns.namespace("pca9554") | pca9554_ns = cg.esphome_ns.namespace("pca9554") | ||||||
|  |  | ||||||
| PCA9554Component = pca9554_ns.class_("PCA9554Component", cg.Component, i2c.I2CDevice) | PCA9554Component = pca9554_ns.class_("PCA9554Component", cg.Component, i2c.I2CDevice) | ||||||
| @@ -23,7 +25,12 @@ PCA9554GPIOPin = pca9554_ns.class_( | |||||||
|  |  | ||||||
| CONF_PCA9554 = "pca9554" | CONF_PCA9554 = "pca9554" | ||||||
| CONFIG_SCHEMA = ( | CONFIG_SCHEMA = ( | ||||||
|     cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA9554Component)}) |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.Required(CONF_ID): cv.declare_id(PCA9554Component), | ||||||
|  |             cv.Optional(CONF_PIN_COUNT, default=8): cv.one_of(4, 8, 16), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|     .extend(cv.COMPONENT_SCHEMA) |     .extend(cv.COMPONENT_SCHEMA) | ||||||
|     .extend( |     .extend( | ||||||
|         i2c.i2c_device_schema(0x20) |         i2c.i2c_device_schema(0x20) | ||||||
| @@ -33,6 +40,7 @@ CONFIG_SCHEMA = ( | |||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     cg.add(var.set_pin_count(config[CONF_PIN_COUNT])) | ||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|     await i2c.register_i2c_device(var, config) |     await i2c.register_i2c_device(var, config) | ||||||
|  |  | ||||||
| @@ -45,11 +53,32 @@ def validate_mode(value): | |||||||
|     return value |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_pin(config): | ||||||
|  |     pca_id = config[CONF_PCA9554].id | ||||||
|  |  | ||||||
|  |     # when pin config validation is performed, the entire YAML has been read, but depending on the component order, | ||||||
|  |     # the pca9554 component may not yet have been processed, so its id property could be either the original string, | ||||||
|  |     # or an ID object. | ||||||
|  |     def matcher(p): | ||||||
|  |         id = p[CONF_ID] | ||||||
|  |         if isinstance(id, ID): | ||||||
|  |             return id.id == pca_id | ||||||
|  |         return id == pca_id | ||||||
|  |  | ||||||
|  |     pca = list(filter(matcher, CORE.raw_config[CONF_PCA9554])) | ||||||
|  |     if not pca: | ||||||
|  |         raise cv.Invalid(f"No pca9554 component found with id matching {pca_id}") | ||||||
|  |     count = pca[0][CONF_PIN_COUNT] | ||||||
|  |     if config[CONF_NUMBER] >= count: | ||||||
|  |         raise cv.Invalid(f"Pin number must be in range 0-{count - 1}") | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| PCA9554_PIN_SCHEMA = cv.All( | PCA9554_PIN_SCHEMA = cv.All( | ||||||
|     { |     { | ||||||
|         cv.GenerateID(): cv.declare_id(PCA9554GPIOPin), |         cv.GenerateID(): cv.declare_id(PCA9554GPIOPin), | ||||||
|         cv.Required(CONF_PCA9554): cv.use_id(PCA9554Component), |         cv.Required(CONF_PCA9554): cv.use_id(PCA9554Component), | ||||||
|         cv.Required(CONF_NUMBER): cv.int_range(min=0, max=8), |         cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), | ||||||
|         cv.Optional(CONF_MODE, default={}): cv.All( |         cv.Optional(CONF_MODE, default={}): cv.All( | ||||||
|             { |             { | ||||||
|                 cv.Optional(CONF_INPUT, default=False): cv.boolean, |                 cv.Optional(CONF_INPUT, default=False): cv.boolean, | ||||||
| @@ -58,7 +87,8 @@ PCA9554_PIN_SCHEMA = cv.All( | |||||||
|             validate_mode, |             validate_mode, | ||||||
|         ), |         ), | ||||||
|         cv.Optional(CONF_INVERTED, default=False): cv.boolean, |         cv.Optional(CONF_INVERTED, default=False): cv.boolean, | ||||||
|     } |     }, | ||||||
|  |     validate_pin, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace pca9554 { | namespace pca9554 { | ||||||
|  |  | ||||||
|  | // for 16 bit expanders, these addresses will be doubled. | ||||||
| const uint8_t INPUT_REG = 0; | const uint8_t INPUT_REG = 0; | ||||||
| const uint8_t OUTPUT_REG = 1; | const uint8_t OUTPUT_REG = 1; | ||||||
| const uint8_t INVERT_REG = 2; | const uint8_t INVERT_REG = 2; | ||||||
| @@ -13,9 +14,10 @@ static const char *const TAG = "pca9554"; | |||||||
|  |  | ||||||
| void PCA9554Component::setup() { | void PCA9554Component::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Setting up PCA9554/PCA9554A..."); |   ESP_LOGCONFIG(TAG, "Setting up PCA9554/PCA9554A..."); | ||||||
|  |   this->reg_width_ = (this->pin_count_ + 7) / 8; | ||||||
|   // Test to see if device exists |   // Test to see if device exists | ||||||
|   if (!this->read_inputs_()) { |   if (!this->read_inputs_()) { | ||||||
|     ESP_LOGE(TAG, "PCA9554 not available under 0x%02X", this->address_); |     ESP_LOGE(TAG, "PCA95xx not detected at 0x%02X", this->address_); | ||||||
|     this->mark_failed(); |     this->mark_failed(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| @@ -44,6 +46,7 @@ void PCA9554Component::loop() { | |||||||
|  |  | ||||||
| void PCA9554Component::dump_config() { | void PCA9554Component::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "PCA9554:"); |   ESP_LOGCONFIG(TAG, "PCA9554:"); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  I/O Pins: %d", this->pin_count_); | ||||||
|   LOG_I2C_DEVICE(this) |   LOG_I2C_DEVICE(this) | ||||||
|   if (this->is_failed()) { |   if (this->is_failed()) { | ||||||
|     ESP_LOGE(TAG, "Communication with PCA9554 failed!"); |     ESP_LOGE(TAG, "Communication with PCA9554 failed!"); | ||||||
| @@ -85,25 +88,33 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) { | |||||||
| } | } | ||||||
|  |  | ||||||
| bool PCA9554Component::read_inputs_() { | bool PCA9554Component::read_inputs_() { | ||||||
|   uint8_t inputs; |   uint8_t inputs[2]; | ||||||
|  |  | ||||||
|   if (this->is_failed()) { |   if (this->is_failed()) { | ||||||
|     ESP_LOGD(TAG, "Device marked failed"); |     ESP_LOGD(TAG, "Device marked failed"); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if ((this->last_error_ = this->read_register(INPUT_REG, &inputs, 1, true)) != esphome::i2c::ERROR_OK) { |   if ((this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_, true)) != | ||||||
|  |       esphome::i2c::ERROR_OK) { | ||||||
|     this->status_set_warning(); |     this->status_set_warning(); | ||||||
|     ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); |     ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   this->status_clear_warning(); |   this->status_clear_warning(); | ||||||
|   this->input_mask_ = inputs; |   this->input_mask_ = inputs[0]; | ||||||
|  |   if (this->reg_width_ == 2) { | ||||||
|  |     this->input_mask_ |= inputs[1] << 8; | ||||||
|  |   } | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
| bool PCA9554Component::write_register_(uint8_t reg, uint8_t value) { | bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { | ||||||
|   if ((this->last_error_ = this->write_register(reg, &value, 1, true)) != esphome::i2c::ERROR_OK) { |   uint8_t outputs[2]; | ||||||
|  |   outputs[0] = (uint8_t) value; | ||||||
|  |   outputs[1] = (uint8_t) (value >> 8); | ||||||
|  |   if ((this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_, true)) != | ||||||
|  |       esphome::i2c::ERROR_OK) { | ||||||
|     this->status_set_warning(); |     this->status_set_warning(); | ||||||
|     ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); |     ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); | ||||||
|     return false; |     return false; | ||||||
|   | |||||||
| @@ -28,19 +28,25 @@ class PCA9554Component : public Component, public i2c::I2CDevice { | |||||||
|  |  | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|  |  | ||||||
|  |   void set_pin_count(size_t pin_count) { this->pin_count_ = pin_count; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool read_inputs_(); |   bool read_inputs_(); | ||||||
|  |  | ||||||
|   bool write_register_(uint8_t reg, uint8_t value); |   bool write_register_(uint8_t reg, uint16_t value); | ||||||
|  |  | ||||||
|  |   /// number of bits the expander has | ||||||
|  |   size_t pin_count_{8}; | ||||||
|  |   /// width of registers | ||||||
|  |   size_t reg_width_{1}; | ||||||
|   /// Mask for the pin config - 1 means OUTPUT, 0 means INPUT |   /// Mask for the pin config - 1 means OUTPUT, 0 means INPUT | ||||||
|   uint8_t config_mask_{0x00}; |   uint16_t config_mask_{0x00}; | ||||||
|   /// The mask to write as output state - 1 means HIGH, 0 means LOW |   /// The mask to write as output state - 1 means HIGH, 0 means LOW | ||||||
|   uint8_t output_mask_{0x00}; |   uint16_t output_mask_{0x00}; | ||||||
|   /// The state of the actual input pin states - 1 means HIGH, 0 means LOW |   /// The state of the actual input pin states - 1 means HIGH, 0 means LOW | ||||||
|   uint8_t input_mask_{0x00}; |   uint16_t input_mask_{0x00}; | ||||||
|   /// Flags to check if read previously during this loop |   /// Flags to check if read previously during this loop | ||||||
|   uint8_t was_previously_read_ = {0x00}; |   uint16_t was_previously_read_ = {0x00}; | ||||||
|   /// Storage for last I2C error seen |   /// Storage for last I2C error seen | ||||||
|   esphome::i2c::ErrorCode last_error_; |   esphome::i2c::ErrorCode last_error_; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -3182,6 +3182,7 @@ pcf8574: | |||||||
|  |  | ||||||
| pca9554: | pca9554: | ||||||
|   - id: pca9554_hub |   - id: pca9554_hub | ||||||
|  |     pin_count: 8 | ||||||
|     address: 0x3F |     address: 0x3F | ||||||
|     i2c_id: i2c_bus |     i2c_id: i2c_bus | ||||||
|  |  | ||||||
| @@ -3331,7 +3332,12 @@ rtttl: | |||||||
| canbus: | canbus: | ||||||
|   - platform: mcp2515 |   - platform: mcp2515 | ||||||
|     id: mcp2515_can |     id: mcp2515_can | ||||||
|     cs_pin: GPIO17 |     cs_pin: | ||||||
|  |       pca9554: pca9554_hub | ||||||
|  |       number: 7 | ||||||
|  |       mode: | ||||||
|  |         output: true | ||||||
|  |       inverted: true | ||||||
|     can_id: 4 |     can_id: 4 | ||||||
|     bit_rate: 50kbps |     bit_rate: 50kbps | ||||||
|     on_frame: |     on_frame: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user