I’m writing a custom component for the Texas Instruments PCA9554 I2C bus expander which I want to use for my sprinkler controller. I’m having some trouble getting it to configure properly in the yaml file, and I was wondering if someone might know what might be going on with either the init.py file or the esp32-sprinkler.yaml file.
I created the PCA9554 custom component using PCF8574 custom component source files already in the esphome component directory. They’re loaded as an external custom component in the esp32-sprinkler.yaml file.
The error I’m getting is:
srodgers@steverod ~/projects/esphome/esp32-sprinkler $ esphome compile esp32-sprinkler.yaml
INFO Reading configuration esp32-sprinkler.yaml...
Failed config
pca9554: [source esp32-sprinkler.yaml:44]
expected a dictionary.
- id: pca9554_master
address: 32
*** All files are attached below ***
My yaml configuration is:
esphome:
name: esp32-sprinkler
platform: ESP32
board: esp32doit-devkit-v1
wifi:
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
- ssid: !secret sec_wifi_ssid
password: !secret sec_wifi_password
web_server:
port: 80
# https://esphome.io/components/web_server.html
logger:
# https://esphome.io/components/logger
api:
password: !secret esphome_api_password
# https://esphome.io/components/api
ota:
password: !secret esphome_ota_password
# https://esphome.io/components/ota
i2c:
sda: 21
scl: 22
scan: true
id: bus_a
external_components:
- source:
type: local
path: custom_components/pca9554
pca9554:
- id: pca9554_stations_1_8
address: 0x20
switch:
- platform: gpio
name: "Station 1"
pin:
pca9554: pca9554_stations_1_8
number: 0
mode:
output: true
inverted: false
The init.py file in the custom components directory is:
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import i2c
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_NUMBER,
CONF_MODE,
CONF_INVERTED,
CONF_OUTPUT,
)
DEPENDENCIES = ["i2c"]
MULTI_CONF = False
pca9554_ns = cg.esphome_ns.namespace("pca9554")
PCA9554Component = pca9554_ns.class_("PCA9554Component", cg.Component, i2c.I2CDevice)
PCA9554GPIOPin = pca9554_ns.class_("PCA9554GPIOPin", cg.GPIOPin)
CONF_PCA9554 = "pca9554"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA9554Component)
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x20))
)
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)
def validate_mode(value):
if not (value[CONF_INPUT] or value[CONF_OUTPUT]):
raise cv.Invalid("Mode must be either input or output")
if value[CONF_INPUT] and value[CONF_OUTPUT]:
raise cv.Invalid("Mode must be either input or output")
return value
PCA9554_PIN_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(PCA9554GPIOPin),
cv.Required(CONF_PCA9554): cv.use_id(PCA9554Component),
cv.Required(CONF_NUMBER): cv.int_range(min=0, max=8),
cv.Optional(CONF_MODE, default={}): cv.All(
{
cv.Optional(CONF_INPUT, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
},
validate_mode,
),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
}
)
@pins.PIN_SCHEMA_REGISTRY.register("pca9554", PCA9554_PIN_SCHEMA)
async def pca9554_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_PCA9554])
cg.add(var.set_parent(parent))
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var
The pca9554.cpp file contains:
#include "pca9554.h"
#include "esphome/core/log.h"
namespace esphome {
namespace pca9554 {
#define INPUT_REG 0
#define OUTPUT_REG 1
#define CONFIG_REG 3
static const char *const TAG = "pca9554";
void PCA9554Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up PCA9554...");
// Test to see if device exists
if (!this->read_inputs_()) {
ESP_LOGE(TAG, "PCA9554 not available under 0x%02X", this->address_);
this->mark_failed();
return;
}
this->config_mask_ = 0; // All inputs at initialization
this->write_config_();
this->output_mask_ = 0; // All outputs low
this->write_outputs_();
this->read_inputs_(); // Read the inputs
}
void PCA9554Component::dump_config() {
ESP_LOGCONFIG(TAG, "PCA9554:");
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with PCA9554 failed!");
}
}
bool PCA9554Component::digital_read(uint8_t pin) {
this->read_inputs_();
return this->input_mask_ & (1 << pin);
}
void PCA9554Component::digital_write(uint8_t pin, bool value) {
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
this->write_outputs_();
}
void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
// Clear mode mask bit
this->config_mask_ &= ~(1 << pin);
// Write GPIO to enable input mode
this->write_config_();
} else if (flags == gpio::FLAG_OUTPUT) {
// Set mode mask bit
this->config_mask_ |= 1 << pin;
}
this->write_config_();
}
bool PCA9554Component::read_inputs_() {
if (this->is_failed())
return false;
bool success;
uint8_t data[1];
data[0] = INPUT_REG;
success = this->write(data, 1, false) == esphome::i2c::ERROR_OK; // Write register address with no stop condition
success = success && (this->read_bytes_raw(data, 1) == esphome::i2c::ERROR_OK);
this->input_mask_ = data[0];
if (!success) {
this->status_set_warning();
return false;
}
this->status_clear_warning();
return true;
}
bool PCA9554Component::write_outputs_() {
if (this->is_failed())
return false;
uint8_t data[2];
data[0] = OUTPUT_REG;
data[1] = this->output_mask_;
if (this->write(data, 2) != esphome::i2c::ERROR_OK) {
this->status_set_warning();
return false;
}
this->status_clear_warning();
return true;
}
bool PCA9554Component::write_config_() {
if (this->is_failed())
return false;
uint8_t data[2];
data[0] = CONFIG_REG;
// Config bits: 1 is an output and 0 is an input
// These have to be inverted per PCA9554 the data sheet. PCA9554: 0 = Output and 1 = input.
data[1] = ~this->config_mask_;
if (this->write(data, 2) != esphome::i2c::ERROR_OK) {
this->status_set_warning();
return false;
}
this->status_clear_warning();
return true;
}
float PCA9554Component::get_setup_priority() const { return setup_priority::IO; }
void PCA9554GPIOPin::setup() { pin_mode(flags_); }
void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void PCA9554GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
std::string PCA9554GPIOPin::dump_summary() const {
char buffer[32];
snprintf(buffer, sizeof(buffer), "%u via PCA9554", pin_);
return buffer;
}
} // namespace pca9554
} // namespace esphome
The pca9554.h file contains:
/// Check i2c availability and setup masks
void setup() override;
/// Helper function to read the value of a pin.
bool digital_read(uint8_t pin);
/// Helper function to write the value of a pin.
void digital_write(uint8_t pin, bool value);
/// Helper function to set the pin mode of a pin.
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void dump_config() override;
protected:
bool read_inputs_();
bool write_outputs_();
bool write_config_();
/// Mask for the pin config - 1 means OUTPUT, 0 means INPUT
uint8_t config_mask_{0x00};
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint8_t output_mask_{0x00};
/// The state of the actual input pin states - 1 means HIGH, 0 means LOW
uint8_t input_mask_{0x00};
};
/// Helper class to expose a PCA9554 pin as an internal input GPIO pin.
class PCA9554GPIOPin : public GPIOPin {
public:
void setup() override;
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
std::string dump_summary() const override;
void set_parent(PCA9554Component *parent) { parent_ = parent; }
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
void set_flags(gpio::Flags flags) { flags_ = flags; }
protected:
PCA9554Component *parent_;
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;
};
} // namespace pca9554
} // namespace esphome