diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index eeb8386e74..c066ae9fb4 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -36,7 +36,7 @@ jobs: python ./script/sync-device_class.py - name: Commit changes - uses: peter-evans/create-pull-request@v7.0.3 + uses: peter-evans/create-pull-request@v7.0.5 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot diff --git a/CODEOWNERS b/CODEOWNERS index f7fbbf9374..1eb13a534b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -86,7 +86,7 @@ esphome/components/cap1188/* @mreditor97 esphome/components/captive_portal/* @OttoWinter esphome/components/ccs811/* @habbie esphome/components/cd74hc4067/* @asoehlke -esphome/components/ch422g/* @jesterret +esphome/components/ch422g/* @clydebarrow @jesterret esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet esphome/components/color_temperature/* @jesserockz @@ -166,6 +166,7 @@ esphome/components/haier/* @paveldn esphome/components/haier/binary_sensor/* @paveldn esphome/components/haier/button/* @paveldn esphome/components/haier/sensor/* @paveldn +esphome/components/haier/switch/* @paveldn esphome/components/haier/text_sensor/* @paveldn esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior @@ -397,6 +398,7 @@ esphome/components/sun_gtil2/* @Mat931 esphome/components/switch/* @esphome/core esphome/components/t6615/* @tylermenezes esphome/components/tca9548a/* @andreashergert1984 +esphome/components/tca9555/* @mobrembski esphome/components/tcl112/* @glmnet esphome/components/tee501/* @Stock-M esphome/components/teleinfo/* @0hax diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7ea52e9a9e..e28b244722 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1242,6 +1242,9 @@ VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configura } resp.available_wake_words.push_back(std::move(resp_wake_word)); } + for (auto &wake_word_id : config.active_wake_words) { + resp.active_wake_words.push_back(wake_word_id); + } resp.max_active_wake_words = config.max_active_wake_words; } return resp; diff --git a/esphome/components/bl0906/sensor.py b/esphome/components/bl0906/sensor.py index bc370c9252..42c6f06092 100644 --- a/esphome/components/bl0906/sensor.py +++ b/esphome/components/bl0906/sensor.py @@ -145,8 +145,9 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( ), ) async def reset_energy_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var async def to_code(config): diff --git a/esphome/components/ch422g/__init__.py b/esphome/components/ch422g/__init__.py index cf8b5f65d3..6a7bace0a2 100644 --- a/esphome/components/ch422g/__init__.py +++ b/esphome/components/ch422g/__init__.py @@ -1,18 +1,20 @@ from esphome import pins import esphome.codegen as cg from esphome.components import i2c +from esphome.components.i2c import I2CBus import esphome.config_validation as cv from esphome.const import ( + CONF_I2C_ID, CONF_ID, CONF_INPUT, CONF_INVERTED, CONF_MODE, CONF_NUMBER, + CONF_OPEN_DRAIN, CONF_OUTPUT, - CONF_RESTORE_VALUE, ) -CODEOWNERS = ["@jesterret"] +CODEOWNERS = ["@jesterret", "@clydebarrow"] DEPENDENCIES = ["i2c"] MULTI_CONF = True ch422g_ns = cg.esphome_ns.namespace("ch422g") @@ -23,29 +25,36 @@ CH422GGPIOPin = ch422g_ns.class_( ) CONF_CH422G = "ch422g" -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(CH422GComponent), - cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, - } - ) - .extend(cv.COMPONENT_SCHEMA) - .extend(i2c.i2c_device_schema(0x24)) -) + +# Note that no address is configurable - each register in the CH422G has a dedicated i2c address +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(CH422GComponent), + cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus), + } +).extend(cv.COMPONENT_SCHEMA) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) + # Can't use register_i2c_device because there is no CONF_ADDRESS + parent = await cg.get_variable(config[CONF_I2C_ID]) + cg.add(var.set_i2c_bus(parent)) + + +# This is used as a final validation step so that modes have been fully transformed. +def pin_mode_check(pin_config, _): + if pin_config[CONF_MODE][CONF_INPUT] and pin_config[CONF_NUMBER] >= 8: + raise cv.Invalid("CH422G only supports input on pins 0-7") + if pin_config[CONF_MODE][CONF_OPEN_DRAIN] and pin_config[CONF_NUMBER] < 8: + raise cv.Invalid("CH422G only supports open drain output on pins 8-11") CH422G_PIN_SCHEMA = pins.gpio_base_schema( CH422GGPIOPin, - cv.int_range(min=0, max=7), - modes=[CONF_INPUT, CONF_OUTPUT], + cv.int_range(min=0, max=11), + modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN], ).extend( { cv.Required(CONF_CH422G): cv.use_id(CH422GComponent), @@ -53,7 +62,7 @@ CH422G_PIN_SCHEMA = pins.gpio_base_schema( ) -@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH422G, CH422G_PIN_SCHEMA) +@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH422G, CH422G_PIN_SCHEMA, pin_mode_check) async def ch422g_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_CH422G]) diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index 25038991ed..0db179d99e 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -4,33 +4,33 @@ namespace esphome { namespace ch422g { -const uint8_t CH422G_REG_IN = 0x26; -const uint8_t CH422G_REG_OUT = 0x38; -const uint8_t OUT_REG_DEFAULT_VAL = 0xdf; +static const uint8_t CH422G_REG_MODE = 0x24; +static const uint8_t CH422G_MODE_OUTPUT = 0x01; // enables output mode on 0-7 +static const uint8_t CH422G_MODE_OPEN_DRAIN = 0x04; // enables open drain mode on 8-11 +static const uint8_t CH422G_REG_IN = 0x26; // read reg for input bits +static const uint8_t CH422G_REG_OUT = 0x38; // write reg for output bits 0-7 +static const uint8_t CH422G_REG_OUT_UPPER = 0x23; // write reg for output bits 8-11 static const char *const TAG = "ch422g"; void CH422GComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up CH422G..."); - // Test to see if device exists - if (!this->read_inputs_()) { + // set outputs before mode + this->write_outputs_(); + // Set mode and check for errors + if (!this->set_mode_(this->mode_value_) || !this->read_inputs_()) { ESP_LOGE(TAG, "CH422G not detected at 0x%02X", this->address_); this->mark_failed(); return; } - // restore defaults over whatever got saved on last boot - if (!this->restore_value_) { - this->write_output_(OUT_REG_DEFAULT_VAL); - } - - ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), - this->status_has_error()); + ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), + this->status_has_error()); } void CH422GComponent::loop() { // Clear all the previously read flags. - this->pin_read_cache_ = 0x00; + this->pin_read_flags_ = 0x00; } void CH422GComponent::dump_config() { @@ -41,82 +41,99 @@ void CH422GComponent::dump_config() { } } -// ch422g doesn't have any flag support (needs docs?) -void CH422GComponent::pin_mode(uint8_t pin, gpio::Flags flags) {} +void CH422GComponent::pin_mode(uint8_t pin, gpio::Flags flags) { + if (pin < 8) { + if (flags & gpio::FLAG_OUTPUT) { + this->mode_value_ |= CH422G_MODE_OUTPUT; + } + } else { + if (flags & gpio::FLAG_OPEN_DRAIN) { + this->mode_value_ |= CH422G_MODE_OPEN_DRAIN; + } + } +} bool CH422GComponent::digital_read(uint8_t pin) { - if (this->pin_read_cache_ == 0 || this->pin_read_cache_ & (1 << pin)) { + if (this->pin_read_flags_ == 0 || this->pin_read_flags_ & (1 << pin)) { // Read values on first access or in case it's being read again in the same loop this->read_inputs_(); } - this->pin_read_cache_ |= (1 << pin); - return this->state_mask_ & (1 << pin); + this->pin_read_flags_ |= (1 << pin); + return (this->input_bits_ & (1 << pin)) != 0; } void CH422GComponent::digital_write(uint8_t pin, bool value) { if (value) { - this->write_output_(this->state_mask_ | (1 << pin)); + this->output_bits_ |= (1 << pin); } else { - this->write_output_(this->state_mask_ & ~(1 << pin)); + this->output_bits_ &= ~(1 << pin); } + this->write_outputs_(); } bool CH422GComponent::read_inputs_() { if (this->is_failed()) { return false; } - - uint8_t temp = 0; - if ((this->last_error_ = this->read(&temp, 1)) != esphome::i2c::ERROR_OK) { - this->status_set_warning(str_sprintf("read_inputs_(): I2C I/O error: %d", (int) this->last_error_).c_str()); - return false; + uint8_t result; + // reading inputs requires the chip to be in input mode, possibly temporarily. + if (this->mode_value_ & CH422G_MODE_OUTPUT) { + this->set_mode_(this->mode_value_ & ~CH422G_MODE_OUTPUT); + result = this->read_reg_(CH422G_REG_IN); + this->set_mode_(this->mode_value_); + } else { + result = this->read_reg_(CH422G_REG_IN); } - - uint8_t output = 0; - if ((this->last_error_ = this->bus_->read(CH422G_REG_IN, &output, 1)) != esphome::i2c::ERROR_OK) { - this->status_set_warning(str_sprintf("read_inputs_(): I2C I/O error: %d", (int) this->last_error_).c_str()); - return false; - } - - this->state_mask_ = output; + this->input_bits_ = result; this->status_clear_warning(); - return true; } -bool CH422GComponent::write_output_(uint8_t value) { - const uint8_t temp = 1; - if ((this->last_error_ = this->write(&temp, 1, false)) != esphome::i2c::ERROR_OK) { - this->status_set_warning(str_sprintf("write_output_(): I2C I/O error: %d", (int) this->last_error_).c_str()); +// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address. +bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) { + auto err = this->bus_->write(reg, &value, 1); + if (err != i2c::ERROR_OK) { + this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str()); return false; } - - uint8_t write_mask = value; - if ((this->last_error_ = this->bus_->write(CH422G_REG_OUT, &write_mask, 1)) != esphome::i2c::ERROR_OK) { - this->status_set_warning( - str_sprintf("write_output_(): I2C I/O error: %d for write_mask: %d", (int) this->last_error_, (int) write_mask) - .c_str()); - return false; - } - - this->state_mask_ = value; this->status_clear_warning(); return true; } +uint8_t CH422GComponent::read_reg_(uint8_t reg) { + uint8_t value; + auto err = this->bus_->read(reg, &value, 1); + if (err != i2c::ERROR_OK) { + this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str()); + return 0; + } + this->status_clear_warning(); + return value; +} + +bool CH422GComponent::set_mode_(uint8_t mode) { return this->write_reg_(CH422G_REG_MODE, mode); } + +bool CH422GComponent::write_outputs_() { + return this->write_reg_(CH422G_REG_OUT, static_cast(this->output_bits_)) && + this->write_reg_(CH422G_REG_OUT_UPPER, static_cast(this->output_bits_ >> 8)); +} + float CH422GComponent::get_setup_priority() const { return setup_priority::IO; } // Run our loop() method very early in the loop, so that we cache read values // before other components call our digital_read() method. float CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI -void CH422GGPIOPin::setup() { pin_mode(flags_); } void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } -bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } -void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); } std::string CH422GGPIOPin::dump_summary() const { return str_sprintf("EXIO%u via CH422G", pin_); } +void CH422GGPIOPin::set_flags(gpio::Flags flags) { + flags_ = flags; + this->parent_->pin_mode(this->pin_, flags); +} } // namespace ch422g } // namespace esphome diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h index 781df65437..30780e09ad 100644 --- a/esphome/components/ch422g/ch422g.h +++ b/esphome/components/ch422g/ch422g.h @@ -23,32 +23,30 @@ class CH422GComponent : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; - float get_loop_priority() const override; - void dump_config() override; - void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } - protected: + bool write_reg_(uint8_t reg, uint8_t value); + uint8_t read_reg_(uint8_t reg); + bool set_mode_(uint8_t mode); bool read_inputs_(); - - bool write_output_(uint8_t value); + bool write_outputs_(); /// The mask to write as output state - 1 means HIGH, 0 means LOW - uint8_t state_mask_{0x00}; + uint16_t output_bits_{0x00}; /// Flags to check if read previously during this loop - uint8_t pin_read_cache_ = {0x00}; - /// Storage for last I2C error seen - esphome::i2c::ErrorCode last_error_; - /// Whether we want to override stored values on expander - bool restore_value_{false}; + uint8_t pin_read_flags_ = {0x00}; + /// Copy of last read values + uint8_t input_bits_ = {0x00}; + /// Copy of the mode value + uint8_t mode_value_{}; }; -/// Helper class to expose a CH422G pin as an internal input GPIO pin. +/// Helper class to expose a CH422G pin as a GPIO pin. class CH422GGPIOPin : public GPIOPin { public: - void setup() override; + void setup() override{}; void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; @@ -57,13 +55,13 @@ class CH422GGPIOPin : public GPIOPin { void set_parent(CH422GComponent *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; } + void set_flags(gpio::Flags flags); protected: - CH422GComponent *parent_; - uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + CH422GComponent *parent_{}; + uint8_t pin_{}; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace ch422g diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index f1420aa127..47058badce 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -147,6 +147,7 @@ void CSE7766Component::parse_data_() { float power = 0.0f; if (power_cycle_exceeds_range) { // Datasheet: power cycle exceeding range means active power is 0 + have_power = true; if (this->power_sensor_ != nullptr) { this->power_sensor_->publish_state(0.0f); } @@ -178,6 +179,15 @@ void CSE7766Component::parse_data_() { if (this->apparent_power_sensor_ != nullptr) { this->apparent_power_sensor_->publish_state(apparent_power); } + if (have_power && this->reactive_power_sensor_ != nullptr) { + const float reactive_power = apparent_power - power; + if (reactive_power < 0.0f) { + ESP_LOGD(TAG, "Impossible reactive power: %.4f is negative", reactive_power); + this->reactive_power_sensor_->publish_state(0.0f); + } else { + this->reactive_power_sensor_->publish_state(reactive_power); + } + } if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) { float pf = NAN; if (apparent_power > 0) { @@ -232,6 +242,7 @@ void CSE7766Component::dump_config() { LOG_SENSOR(" ", "Power", this->power_sensor_); LOG_SENSOR(" ", "Energy", this->energy_sensor_); LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_); + LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_); LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_); this->check_uart_settings(4800); } diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 0b724d6bbb..5d89b3b75b 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -16,6 +16,9 @@ class CSE7766Component : public Component, public uart::UARTDevice { void set_apparent_power_sensor(sensor::Sensor *apparent_power_sensor) { apparent_power_sensor_ = apparent_power_sensor; } + void set_reactive_power_sensor(sensor::Sensor *reactive_power_sensor) { + reactive_power_sensor_ = reactive_power_sensor; + } void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } void loop() override; @@ -35,6 +38,7 @@ class CSE7766Component : public Component, public uart::UARTDevice { sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *energy_sensor_{nullptr}; sensor::Sensor *apparent_power_sensor_{nullptr}; + sensor::Sensor *reactive_power_sensor_{nullptr}; sensor::Sensor *power_factor_sensor_{nullptr}; uint32_t cf_pulses_total_{0}; uint16_t cf_pulses_last_{0}; diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index b64dcf7de3..ecb59c4b5f 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -8,18 +8,21 @@ from esphome.const import ( CONF_ID, CONF_POWER, CONF_POWER_FACTOR, + CONF_REACTIVE_POWER, CONF_VOLTAGE, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_VOLT, UNIT_VOLT_AMPS, + UNIT_VOLT_AMPS_REACTIVE, UNIT_WATT, UNIT_WATT_HOURS, ) @@ -62,6 +65,12 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_APPARENT_POWER, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_REACTIVE_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( accuracy_decimals=2, device_class=DEVICE_CLASS_POWER_FACTOR, @@ -94,6 +103,9 @@ async def to_code(config): if apparent_power_config := config.get(CONF_APPARENT_POWER): sens = await sensor.new_sensor(apparent_power_config) cg.add(var.set_apparent_power_sensor(sens)) + if reactive_power_config := config.get(CONF_REACTIVE_POWER): + sens = await sensor.new_sensor(reactive_power_config) + cg.add(var.set_reactive_power_sensor(sens)) if power_factor_config := config.get(CONF_POWER_FACTOR): sens = await sensor.new_sensor(power_factor_config) cg.add(var.set_power_factor_sensor(sens)) diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 705dff0f1b..ecc07d4c91 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -1,7 +1,8 @@ +from esphome import automation import esphome.codegen as cg from esphome.components import binary_sensor, esp32_ble_server, output import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID AUTO_LOAD = ["esp32_ble_server"] CODEOWNERS = ["@jesserockz"] @@ -11,13 +12,36 @@ CONF_AUTHORIZED_DURATION = "authorized_duration" CONF_AUTHORIZER = "authorizer" CONF_BLE_SERVER_ID = "ble_server_id" CONF_IDENTIFY_DURATION = "identify_duration" +CONF_ON_PROVISIONED = "on_provisioned" +CONF_ON_PROVISIONING = "on_provisioning" +CONF_ON_START = "on_start" +CONF_ON_STOP = "on_stop" CONF_STATUS_INDICATOR = "status_indicator" CONF_WIFI_TIMEOUT = "wifi_timeout" +improv_ns = cg.esphome_ns.namespace("improv") +Error = improv_ns.enum("Error") +State = improv_ns.enum("State") + esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv") ESP32ImprovComponent = esp32_improv_ns.class_( "ESP32ImprovComponent", cg.Component, esp32_ble_server.BLEServiceComponent ) +ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_( + "ESP32ImprovProvisionedTrigger", automation.Trigger.template() +) +ESP32ImprovProvisioningTrigger = esp32_improv_ns.class_( + "ESP32ImprovProvisioningTrigger", automation.Trigger.template() +) +ESP32ImprovStartTrigger = esp32_improv_ns.class_( + "ESP32ImprovStartTrigger", automation.Trigger.template() +) +ESP32ImprovStateTrigger = esp32_improv_ns.class_( + "ESP32ImprovStateTrigger", automation.Trigger.template() +) +ESP32ImprovStoppedTrigger = esp32_improv_ns.class_( + "ESP32ImprovStoppedTrigger", automation.Trigger.template() +) CONFIG_SCHEMA = cv.Schema( @@ -37,6 +61,37 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional( CONF_WIFI_TIMEOUT, default="1min" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32ImprovProvisionedTrigger + ), + } + ), + cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32ImprovProvisioningTrigger + ), + } + ), + cv.Optional(CONF_ON_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger), + } + ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger), + } + ), + cv.Optional(CONF_ON_STOP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32ImprovStoppedTrigger + ), + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -63,3 +118,29 @@ async def to_code(config): if CONF_STATUS_INDICATOR in config: status_indicator = await cg.get_variable(config[CONF_STATUS_INDICATOR]) cg.add(var.set_status_indicator(status_indicator)) + + use_state_callback = False + for conf in config.get(CONF_ON_PROVISIONED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + use_state_callback = True + for conf in config.get(CONF_ON_PROVISIONING, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + use_state_callback = True + for conf in config.get(CONF_ON_START, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + use_state_callback = True + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(State, "state"), (Error, "error")], conf + ) + use_state_callback = True + for conf in config.get(CONF_ON_STOP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + use_state_callback = True + if use_state_callback: + cg.add_define("USE_ESP32_IMPROV_STATE_CALLBACK") diff --git a/esphome/components/esp32_improv/automation.h b/esphome/components/esp32_improv/automation.h new file mode 100644 index 0000000000..52c5da125b --- /dev/null +++ b/esphome/components/esp32_improv/automation.h @@ -0,0 +1,72 @@ +#pragma once +#ifdef USE_ESP32 +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK +#include "esp32_improv_component.h" + +#include "esphome/core/automation.h" + +#include + +namespace esphome { +namespace esp32_improv { + +class ESP32ImprovProvisionedTrigger : public Trigger<> { + public: + explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) { + parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONED && !parent->is_failed()) { + trigger(); + } + }); + } +}; + +class ESP32ImprovProvisioningTrigger : public Trigger<> { + public: + explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) { + parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONING && !parent->is_failed()) { + trigger(); + } + }); + } +}; + +class ESP32ImprovStartTrigger : public Trigger<> { + public: + explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) { + parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) && + !parent->is_failed()) { + trigger(); + } + }); + } +}; + +class ESP32ImprovStateTrigger : public Trigger { + public: + explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) { + parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + if (!parent->is_failed()) { + trigger(state, error); + } + }); + } +}; + +class ESP32ImprovStoppedTrigger : public Trigger<> { + public: + explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) { + parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + if (state == improv::STATE_STOPPED && !parent->is_failed()) { + trigger(); + } + }); + } +}; + +} // namespace esp32_improv +} // namespace esphome +#endif +#endif diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index d90eaac3b6..d36b50feb0 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -68,7 +68,12 @@ void ESP32ImprovComponent::setup_characteristics() { void ESP32ImprovComponent::loop() { if (!global_ble_server->is_running()) { - this->state_ = improv::STATE_STOPPED; + if (this->state_ != improv::STATE_STOPPED) { + this->state_ = improv::STATE_STOPPED; +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK + this->state_callback_.call(this->state_, this->error_state_); +#endif + } this->incoming_data_.clear(); return; } @@ -217,6 +222,9 @@ void ESP32ImprovComponent::set_state_(improv::State state) { service_data[7] = 0x00; // Reserved esp32_ble::global_ble->advertising_set_service_data(service_data); +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK + this->state_callback_.call(this->state_, this->error_state_); +#endif } void ESP32ImprovComponent::set_error_(improv::Error error) { @@ -270,7 +278,7 @@ void ESP32ImprovComponent::dump_config() { void ESP32ImprovComponent::process_incoming_data_() { uint8_t length = this->incoming_data_[1]; - ESP_LOGD(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str()); + ESP_LOGV(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str()); if (this->incoming_data_.size() - 3 == length) { this->set_error_(improv::ERROR_NONE); improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_); @@ -295,7 +303,7 @@ void ESP32ImprovComponent::process_incoming_data_() { wifi::global_wifi_component->set_sta(sta); wifi::global_wifi_component->start_connecting(sta, false); this->set_state_(improv::STATE_PROVISIONING); - ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), + ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this); @@ -313,7 +321,7 @@ void ESP32ImprovComponent::process_incoming_data_() { this->incoming_data_.clear(); } } else if (this->incoming_data_.size() - 2 > length) { - ESP_LOGV(TAG, "Too much data came in, or malformed resetting buffer..."); + ESP_LOGV(TAG, "Too much data received or data malformed; resetting buffer..."); this->incoming_data_.clear(); } else { ESP_LOGV(TAG, "Waiting for split data packets..."); @@ -327,7 +335,7 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() { if (this->authorizer_ != nullptr) this->authorized_start_ = millis(); #endif - ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); + ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network"); wifi::global_wifi_component->clear_sta(); } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 3ed377a6ad..062b3f585b 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -9,6 +9,10 @@ #include "esphome/components/esp32_ble_server/ble_server.h" #include "esphome/components/wifi/wifi_component.h" +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK +#include "esphome/core/automation.h" +#endif + #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif @@ -42,6 +46,11 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { void stop() override; bool is_active() const { return this->state_ != improv::STATE_STOPPED; } +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK + void add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); + } +#endif #ifdef USE_BINARY_SENSOR void set_authorizer(binary_sensor::BinarySensor *authorizer) { this->authorizer_ = authorizer; } #endif @@ -54,6 +63,9 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { void set_wifi_timeout(uint32_t wifi_timeout) { this->wifi_timeout_ = wifi_timeout; } uint32_t get_wifi_timeout() const { return this->wifi_timeout_; } + improv::State get_improv_state() const { return this->state_; } + improv::Error get_improv_error_state() const { return this->error_state_; } + protected: bool should_start_{false}; bool setup_complete_{false}; @@ -84,6 +96,9 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { improv::State state_{improv::STATE_STOPPED}; improv::Error error_state_{improv::ERROR_NONE}; +#ifdef USE_ESP32_IMPROV_STATE_CALLBACK + CallbackManager state_callback_{}; +#endif bool status_indicator_state_{false}; void set_status_indicator_state_(bool state); diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 1c6acda724..475d60df53 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -59,6 +59,7 @@ ETHERNET_TYPES = { "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081, "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, "W5500": EthernetType.ETHERNET_TYPE_W5500, + "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, } SPI_ETHERNET_TYPES = ["W5500"] @@ -171,6 +172,7 @@ CONFIG_SCHEMA = cv.All( "KSZ8081": RMII_SCHEMA, "KSZ8081RNA": RMII_SCHEMA, "W5500": SPI_SCHEMA, + "OPENETH": BASE_SCHEMA, }, upper=True, ), @@ -240,6 +242,9 @@ async def to_code(config): if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) add_idf_sdkconfig_option("CONFIG_ETH_SPI_ETHERNET_W5500", True) + elif config[CONF_TYPE] == "OPENETH": + cg.add_define("USE_ETHERNET_OPENETH") + add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True) else: cg.add(var.set_phy_addr(config[CONF_PHY_ADDR])) cg.add(var.set_mdc_pin(config[CONF_MDC_PIN])) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index fdb6eb2da0..00c7ae4ab8 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -120,6 +120,8 @@ void EthernetComponent::setup() { phy_config.reset_gpio_num = this->reset_pin_; esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); +#elif defined(USE_ETHERNET_OPENETH) + esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config); #else phy_config.phy_addr = this->phy_addr_; phy_config.reset_gpio_num = this->power_pin_; @@ -143,6 +145,13 @@ void EthernetComponent::setup() { #endif switch (this->type_) { +#ifdef USE_ETHERNET_OPENETH + case ETHERNET_TYPE_OPENETH: { + phy_config.autonego_timeout_ms = 1000; + this->phy_ = esp_eth_phy_new_dp83848(&phy_config); + break; + } +#endif #if CONFIG_ETH_USE_ESP32_EMAC case ETHERNET_TYPE_LAN8720: { this->phy_ = esp_eth_phy_new_lan87xx(&phy_config); @@ -302,6 +311,10 @@ void EthernetComponent::dump_config() { eth_type = "W5500"; break; + case ETHERNET_TYPE_OPENETH: + eth_type = "OPENETH"; + break; + default: eth_type = "Unknown"; break; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index f0fe6cab87..5ee430c046 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -25,6 +25,7 @@ enum EthernetType { ETHERNET_TYPE_KSZ8081, ETHERNET_TYPE_KSZ8081RNA, ETHERNET_TYPE_W5500, + ETHERNET_TYPE_OPENETH, }; struct ManualIP { diff --git a/esphome/components/gpio_expander/__init__.py b/esphome/components/gpio_expander/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/gpio_expander/cached_gpio.h b/esphome/components/gpio_expander/cached_gpio.h new file mode 100644 index 0000000000..784c5f0f4a --- /dev/null +++ b/esphome/components/gpio_expander/cached_gpio.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include "esphome/core/hal.h" + +namespace esphome { +namespace gpio_expander { + +/// @brief A class to cache the read state of a GPIO expander. +template class CachedGpioExpander { + public: + bool digital_read(T pin) { + if (!this->read_cache_invalidated_[pin]) { + this->read_cache_invalidated_[pin] = true; + return this->digital_read_cache(pin); + } + return this->digital_read_hw(pin); + } + + void digital_write(T pin, bool value) { this->digital_write_hw(pin, value); } + + protected: + virtual bool digital_read_hw(T pin) = 0; + virtual bool digital_read_cache(T pin) = 0; + virtual void digital_write_hw(T pin, bool value) = 0; + + void reset_pin_cache_() { + for (T i = 0; i < N; i++) { + this->read_cache_invalidated_[i] = false; + } + } + + std::array read_cache_invalidated_{}; +}; + +} // namespace gpio_expander +} // namespace esphome diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index f7423a1356..f2dc7174cb 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -114,7 +114,6 @@ SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = { SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = { "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY, "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, - "ECO": ClimatePreset.CLIMATE_PRESET_ECO, "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, } @@ -240,7 +239,9 @@ CONFIG_SCHEMA = cv.All( ): cv.ensure_list( cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True) ), - cv.Optional(CONF_BEEPER, default=True): cv.boolean, + cv.Optional(CONF_BEEPER): cv.invalid( + f"The {CONF_BEEPER} option is deprecated, use beeper_on/beeper_off actions or beeper switch for a haier platform instead" + ), cv.Optional( CONF_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE ): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50), @@ -254,7 +255,7 @@ CONFIG_SCHEMA = cv.All( ): cv.int_range(min=PROTOCOL_STATUS_MESSAGE_HEADER_SIZE), cv.Optional( CONF_SUPPORTED_PRESETS, - default=["BOOST", "ECO", "SLEEP"], # No AWAY by default + default=["BOOST", "SLEEP"], # No AWAY by default ): cv.ensure_list( cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True) ), diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 0bd3863160..ba80c1ca1b 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -52,8 +52,6 @@ bool check_timeout(std::chrono::steady_clock::time_point now, std::chrono::stead HaierClimateBase::HaierClimateBase() : haier_protocol_(*this), protocol_phase_(ProtocolPhases::SENDING_INIT_1), - display_status_(true), - health_mode_(false), force_send_control_(false), forced_request_status_(false), reset_protocol_request_(false), @@ -127,21 +125,34 @@ haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_() { } #endif -bool HaierClimateBase::get_display_state() const { return this->display_status_; } - -void HaierClimateBase::set_display_state(bool state) { - if (this->display_status_ != state) { - this->display_status_ = state; - this->force_send_control_ = true; +void HaierClimateBase::save_settings() { + HaierBaseSettings settings{this->get_health_mode(), this->get_display_state()}; + if (!this->base_rtc_.save(&settings)) { + ESP_LOGW(TAG, "Failed to save settings"); } } -bool HaierClimateBase::get_health_mode() const { return this->health_mode_; } +bool HaierClimateBase::get_display_state() const { + return (this->display_status_ == SwitchState::ON) || (this->display_status_ == SwitchState::PENDING_ON); +} + +void HaierClimateBase::set_display_state(bool state) { + if (state != this->get_display_state()) { + this->display_status_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; + this->force_send_control_ = true; + this->save_settings(); + } +} + +bool HaierClimateBase::get_health_mode() const { + return (this->health_mode_ == SwitchState::ON) || (this->health_mode_ == SwitchState::PENDING_ON); +} void HaierClimateBase::set_health_mode(bool state) { - if (this->health_mode_ != state) { - this->health_mode_ = state; + if (state != this->get_health_mode()) { + this->health_mode_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; this->force_send_control_ = true; + this->save_settings(); } } @@ -287,6 +298,14 @@ void HaierClimateBase::loop() { } this->process_phase(now); this->haier_protocol_.loop(); +#ifdef USE_SWITCH + if ((this->display_switch_ != nullptr) && (this->display_switch_->state != this->get_display_state())) { + this->display_switch_->publish_state(this->get_display_state()); + } + if ((this->health_mode_switch_ != nullptr) && (this->health_mode_switch_->state != this->get_health_mode())) { + this->health_mode_switch_->publish_state(this->get_health_mode()); + } +#endif // USE_SWITCH } void HaierClimateBase::process_protocol_reset() { @@ -329,6 +348,26 @@ bool HaierClimateBase::prepare_pending_action() { ClimateTraits HaierClimateBase::traits() { return traits_; } +void HaierClimateBase::initialization() { + constexpr uint32_t restore_settings_version = 0xA77D21EF; + this->base_rtc_ = + global_preferences->make_preference(this->get_object_id_hash() ^ restore_settings_version); + HaierBaseSettings recovered; + if (!this->base_rtc_.load(&recovered)) { + recovered = {false, true}; + } + this->display_status_ = recovered.display_state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; + this->health_mode_ = recovered.health_mode ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; +#ifdef USE_SWITCH + if (this->display_switch_ != nullptr) { + this->display_switch_->publish_state(this->get_display_state()); + } + if (this->health_mode_switch_ != nullptr) { + this->health_mode_switch_->publish_state(this->get_health_mode()); + } +#endif +} + void HaierClimateBase::control(const ClimateCall &call) { ESP_LOGD("Control", "Control call"); if (!this->valid_connection()) { @@ -353,6 +392,22 @@ void HaierClimateBase::control(const ClimateCall &call) { } } +#ifdef USE_SWITCH +void HaierClimateBase::set_display_switch(switch_::Switch *sw) { + this->display_switch_ = sw; + if ((this->display_switch_ != nullptr) && (this->valid_connection())) { + this->display_switch_->publish_state(this->get_display_state()); + } +} + +void HaierClimateBase::set_health_mode_switch(switch_::Switch *sw) { + this->health_mode_switch_ = sw; + if ((this->health_mode_switch_ != nullptr) && (this->valid_connection())) { + this->health_mode_switch_->publish_state(this->get_health_mode()); + } +} +#endif + void HaierClimateBase::HvacSettings::reset() { this->valid = false; this->mode.reset(); diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index 7d92a6611c..f0597c49ff 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -8,6 +8,10 @@ // HaierProtocol #include +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif + namespace esphome { namespace haier { @@ -20,10 +24,24 @@ enum class ActionRequest : uint8_t { START_STERI_CLEAN = 5, // only hOn }; +struct HaierBaseSettings { + bool health_mode; + bool display_state; +}; + class HaierClimateBase : public esphome::Component, public esphome::climate::Climate, public esphome::uart::UARTDevice, public haier_protocol::ProtocolStream { +#ifdef USE_SWITCH + public: + void set_display_switch(switch_::Switch *sw); + void set_health_mode_switch(switch_::Switch *sw); + + protected: + switch_::Switch *display_switch_{nullptr}; + switch_::Switch *health_mode_switch_{nullptr}; +#endif public: HaierClimateBase(); HaierClimateBase(const HaierClimateBase &) = delete; @@ -82,7 +100,8 @@ class HaierClimateBase : public esphome::Component, virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; virtual haier_protocol::HaierMessage get_control_message() = 0; // NOLINT(readability-identifier-naming) virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; // NOLINT(readability-identifier-naming) - virtual void initialization(){}; + virtual void save_settings(); + virtual void initialization(); virtual bool prepare_pending_action(); virtual void process_protocol_reset(); esphome::climate::ClimateTraits traits() override; @@ -127,13 +146,19 @@ class HaierClimateBase : public esphome::Component, ActionRequest action; esphome::optional message; }; + enum class SwitchState { + OFF = 0b00, + ON = 0b01, + PENDING_OFF = 0b10, + PENDING_ON = 0b11, + }; haier_protocol::ProtocolHandler haier_protocol_; ProtocolPhases protocol_phase_; esphome::optional action_request_; uint8_t fan_mode_speed_; uint8_t other_modes_fan_speed_; - bool display_status_; - bool health_mode_; + SwitchState display_status_{SwitchState::ON}; + SwitchState health_mode_{SwitchState::OFF}; bool force_send_control_; bool forced_request_status_; bool reset_protocol_request_; @@ -148,6 +173,7 @@ class HaierClimateBase : public esphome::Component, std::chrono::steady_clock::time_point last_status_request_; // To request AC status std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level CallbackManager status_message_callback_{}; + ESPPreferenceObject base_rtc_; }; class StatusMessageTrigger : public Trigger { diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index a1c5098cec..e7be1fa418 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -31,9 +31,32 @@ HonClimate::HonClimate() HonClimate::~HonClimate() {} -void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; } +void HonClimate::set_beeper_state(bool state) { + if (state != this->settings_.beeper_state) { + this->settings_.beeper_state = state; +#ifdef USE_SWITCH + this->beeper_switch_->publish_state(state); +#endif + this->hon_rtc_.save(&this->settings_); + } +} -bool HonClimate::get_beeper_state() const { return this->beeper_status_; } +bool HonClimate::get_beeper_state() const { return this->settings_.beeper_state; } + +void HonClimate::set_quiet_mode_state(bool state) { + if (state != this->get_quiet_mode_state()) { + this->quiet_mode_state_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; + this->settings_.quiet_mode_state = state; +#ifdef USE_SWITCH + this->quiet_mode_switch_->publish_state(state); +#endif + this->hon_rtc_.save(&this->settings_); + } +} + +bool HonClimate::get_quiet_mode_state() const { + return (this->quiet_mode_state_ == SwitchState::ON) || (this->quiet_mode_state_ == SwitchState::PENDING_ON); +} esphome::optional HonClimate::get_vertical_airflow() const { return this->current_vertical_swing_; @@ -474,16 +497,19 @@ haier_protocol::HaierMessage HonClimate::get_power_message(bool state) { } void HonClimate::initialization() { - constexpr uint32_t restore_settings_version = 0xE834D8DCUL; - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash() ^ restore_settings_version); + HaierClimateBase::initialization(); + constexpr uint32_t restore_settings_version = 0x57EB59DDUL; + this->hon_rtc_ = + global_preferences->make_preference(this->get_object_id_hash() ^ restore_settings_version); HonSettings recovered; - if (this->rtc_.load(&recovered)) { + if (this->hon_rtc_.load(&recovered)) { this->settings_ = recovered; } else { - this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER}; + this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER, true, false}; } this->current_vertical_swing_ = this->settings_.last_vertiacal_swing; this->current_horizontal_swing_ = this->settings_.last_horizontal_swing; + this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF; } haier_protocol::HaierMessage HonClimate::get_control_message() { @@ -519,8 +545,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { out_data->ac_power = 1; out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::FAN; out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode - // Disabling boost and eco mode for Fan only - out_data->quiet_mode = 0; + // Disabling boost for Fan only out_data->fast_mode = 0; break; case CLIMATE_MODE_COOL: @@ -582,47 +607,34 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { } if (out_data->ac_power == 0) { // If AC is off - no presets allowed - out_data->quiet_mode = 0; out_data->fast_mode = 0; out_data->sleep_mode = 0; } else if (climate_control.preset.has_value()) { switch (climate_control.preset.value()) { case CLIMATE_PRESET_NONE: - out_data->quiet_mode = 0; - out_data->fast_mode = 0; - out_data->sleep_mode = 0; - out_data->ten_degree = 0; - break; - case CLIMATE_PRESET_ECO: - // Eco is not supported in Fan only mode - out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; out_data->fast_mode = 0; out_data->sleep_mode = 0; out_data->ten_degree = 0; break; case CLIMATE_PRESET_BOOST: - out_data->quiet_mode = 0; // Boost is not supported in Fan only mode out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; out_data->sleep_mode = 0; out_data->ten_degree = 0; break; case CLIMATE_PRESET_AWAY: - out_data->quiet_mode = 0; out_data->fast_mode = 0; out_data->sleep_mode = 0; // 10 degrees allowed only in heat mode out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0; break; case CLIMATE_PRESET_SLEEP: - out_data->quiet_mode = 0; out_data->fast_mode = 0; out_data->sleep_mode = 1; out_data->ten_degree = 0; break; default: ESP_LOGE("Control", "Unsupported preset"); - out_data->quiet_mode = 0; out_data->fast_mode = 0; out_data->sleep_mode = 0; out_data->ten_degree = 0; @@ -638,10 +650,23 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value(); this->pending_horizontal_direction_.reset(); } - out_data->beeper_status = ((!this->beeper_status_) || (!has_hvac_settings)) ? 1 : 0; + { + // Quiet mode + if ((out_data->ac_power == 0) || (out_data->ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN)) { + // If AC is off or in fan only mode - no quiet mode allowed + out_data->quiet_mode = 0; + } else { + out_data->quiet_mode = this->get_quiet_mode_state() ? 1 : 0; + } + // Clean quiet mode state pending flag + this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01); + } + out_data->beeper_status = ((!this->get_beeper_state()) || (!has_hvac_settings)) ? 1 : 0; control_out_buffer[4] = 0; // This byte should be cleared before setting values - out_data->display_status = this->display_status_ ? 1 : 0; - out_data->health_mode = this->health_mode_ ? 1 : 0; + out_data->display_status = this->get_display_state() ? 1 : 0; + this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01); + out_data->health_mode = this->get_health_mode() ? 1 : 0; + this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01); return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, control_out_buffer, this->real_control_packet_size_); @@ -765,6 +790,22 @@ void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::stri } #endif // USE_TEXT_SENSOR +#ifdef USE_SWITCH +void HonClimate::set_beeper_switch(switch_::Switch *sw) { + this->beeper_switch_ = sw; + if (this->beeper_switch_ != nullptr) { + this->beeper_switch_->publish_state(this->get_beeper_state()); + } +} + +void HonClimate::set_quiet_mode_switch(switch_::Switch *sw) { + this->quiet_mode_switch_ = sw; + if (this->quiet_mode_switch_ != nullptr) { + this->quiet_mode_switch_->publish_state(this->settings_.quiet_mode_state); + } +} +#endif // USE_SWITCH + haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { size_t expected_size = 2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_; @@ -827,9 +868,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * { // Extra modes/presets optional old_preset = this->preset; - if (packet.control.quiet_mode != 0) { - this->preset = CLIMATE_PRESET_ECO; - } else if (packet.control.fast_mode != 0) { + if (packet.control.fast_mode != 0) { this->preset = CLIMATE_PRESET_BOOST; } else if (packet.control.sleep_mode != 0) { this->preset = CLIMATE_PRESET_SLEEP; @@ -883,28 +922,26 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * } should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); } - { - // Display status - // should be before "Climate mode" because it is changing this->mode - if (packet.control.ac_power != 0) { - // if AC is off display status always ON so process it only when AC is on - bool disp_status = packet.control.display_status != 0; - if (disp_status != this->display_status_) { - // Do something only if display status changed - if (this->mode == CLIMATE_MODE_OFF) { - // AC just turned on from remote need to turn off display - this->force_send_control_ = true; - } else { - this->display_status_ = disp_status; - } + // Display status + // should be before "Climate mode" because it is changing this->mode + if (packet.control.ac_power != 0) { + // if AC is off display status always ON so process it only when AC is on + bool disp_status = packet.control.display_status != 0; + if (disp_status != this->get_display_state()) { + // Do something only if display status changed + if (this->mode == CLIMATE_MODE_OFF) { + // AC just turned on from remote need to turn off display + this->force_send_control_ = true; + } else if ((((uint8_t) this->health_mode_) & 0b10) == 0) { + this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF; } } } - { - // Health mode - bool old_health_mode = this->health_mode_; - this->health_mode_ = packet.control.health_mode == 1; - should_publish = should_publish || (old_health_mode != this->health_mode_); + // Health mode + if ((((uint8_t) this->health_mode_) & 0b10) == 0) { + bool old_health_mode = this->get_health_mode(); + this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF; + should_publish = should_publish || (old_health_mode != this->get_health_mode()); } { CleaningState new_cleaning; @@ -958,17 +995,36 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * } should_publish = should_publish || (old_mode != this->mode); } + { + // Quiet mode, should be after climate mode + if ((this->mode != CLIMATE_MODE_FAN_ONLY) && (this->mode != CLIMATE_MODE_OFF) && + ((((uint8_t) this->quiet_mode_state_) & 0b10) == 0)) { + // In proper mode and not in pending state + bool new_quiet_mode = packet.control.quiet_mode != 0; + if (new_quiet_mode != this->get_quiet_mode_state()) { + this->quiet_mode_state_ = new_quiet_mode ? SwitchState::ON : SwitchState::OFF; + this->settings_.quiet_mode_state = new_quiet_mode; + this->hon_rtc_.save(&this->settings_); + } + } + } { // Swing mode ClimateSwingMode old_swing_mode = this->swing_mode; - if (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) { - if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) { + const std::set &swing_modes = traits_.get_supported_swing_modes(); + bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end(); + bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end(); + if (horizontal_swing_supported && + (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { + if (vertical_swing_supported && + (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO)) { this->swing_mode = CLIMATE_SWING_BOTH; } else { this->swing_mode = CLIMATE_SWING_HORIZONTAL; } } else { - if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) { + if (vertical_swing_supported && + (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO)) { this->swing_mode = CLIMATE_SWING_VERTICAL; } else { this->swing_mode = CLIMATE_SWING_OFF; @@ -985,7 +1041,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * if (save_settings) { this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value(); this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value(); - this->rtc_.save(&this->settings_); + this->hon_rtc_.save(&this->settings_); } should_publish = should_publish || (old_swing_mode != this->swing_mode); } @@ -1017,7 +1073,7 @@ void HonClimate::fill_control_messages_queue_() { haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::BEEPER_STATUS, - this->beeper_status_ ? ZERO_BUF : ONE_BUF, 2)); + this->get_beeper_state() ? ZERO_BUF : ONE_BUF, 2)); } // Health mode { @@ -1025,13 +1081,16 @@ void HonClimate::fill_control_messages_queue_() { haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::HEALTH_MODE, - this->health_mode_ ? ONE_BUF : ZERO_BUF, 2)); + this->get_health_mode() ? ONE_BUF : ZERO_BUF, 2)); + this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01); } // Climate mode + ClimateMode climate_mode = this->mode; bool new_power = this->mode != CLIMATE_MODE_OFF; uint8_t fan_mode_buf[] = {0x00, 0xFF}; uint8_t quiet_mode_buf[] = {0x00, 0xFF}; if (climate_control.mode.has_value()) { + climate_mode = climate_control.mode.value(); uint8_t buffer[2] = {0x00, 0x00}; switch (climate_control.mode.value()) { case CLIMATE_MODE_OFF: @@ -1076,8 +1135,6 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::AC_MODE, buffer, 2)); fan_mode_buf[1] = this->other_modes_fan_speed_; // Auto doesn't work in fan only mode - // Disabling eco mode for Fan only - quiet_mode_buf[1] = 0; break; case CLIMATE_MODE_COOL: new_power = true; @@ -1108,30 +1165,20 @@ void HonClimate::fill_control_messages_queue_() { uint8_t away_mode_buf[] = {0x00, 0xFF}; if (!new_power) { // If AC is off - no presets allowed - quiet_mode_buf[1] = 0x00; fast_mode_buf[1] = 0x00; away_mode_buf[1] = 0x00; } else if (climate_control.preset.has_value()) { switch (climate_control.preset.value()) { case CLIMATE_PRESET_NONE: - quiet_mode_buf[1] = 0x00; - fast_mode_buf[1] = 0x00; - away_mode_buf[1] = 0x00; - break; - case CLIMATE_PRESET_ECO: - // Eco is not supported in Fan only mode - quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; fast_mode_buf[1] = 0x00; away_mode_buf[1] = 0x00; break; case CLIMATE_PRESET_BOOST: - quiet_mode_buf[1] = 0x00; // Boost is not supported in Fan only mode fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; away_mode_buf[1] = 0x00; break; case CLIMATE_PRESET_AWAY: - quiet_mode_buf[1] = 0x00; fast_mode_buf[1] = 0x00; away_mode_buf[1] = (this->mode == CLIMATE_MODE_HEAT) ? 0x01 : 0x00; break; @@ -1140,8 +1187,18 @@ void HonClimate::fill_control_messages_queue_() { break; } } + { + // Quiet mode + if (new_power && (climate_mode != CLIMATE_MODE_FAN_ONLY) && this->get_quiet_mode_state()) { + quiet_mode_buf[1] = 0x01; + } else { + quiet_mode_buf[1] = 0x00; + } + // Clean quiet mode state pending flag + this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01); + } auto presets = this->traits_.get_supported_presets(); - if ((quiet_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_ECO) != presets.end()))) { + if (quiet_mode_buf[1] != 0xFF) { this->control_messages_queue_.push( haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 64c54186ed..58173f8154 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -10,6 +10,9 @@ #ifdef USE_TEXT_SENSOR #include "esphome/components/text_sensor/text_sensor.h" #endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif #include "esphome/core/automation.h" #include "haier_base.h" #include "hon_packet.h" @@ -28,6 +31,8 @@ enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE struct HonSettings { hon_protocol::VerticalSwingMode last_vertiacal_swing; hon_protocol::HorizontalSwingMode last_horizontal_swing; + bool beeper_state; + bool quiet_mode_state; }; class HonClimate : public HaierClimateBase { @@ -86,6 +91,15 @@ class HonClimate : public HaierClimateBase { protected: void update_sub_text_sensor_(SubTextSensorType type, const std::string &value); text_sensor::TextSensor *sub_text_sensors_[(size_t) SubTextSensorType::SUB_TEXT_SENSOR_TYPE_COUNT]{nullptr}; +#endif +#ifdef USE_SWITCH + public: + void set_beeper_switch(switch_::Switch *sw); + void set_quiet_mode_switch(switch_::Switch *sw); + + protected: + switch_::Switch *beeper_switch_{nullptr}; + switch_::Switch *quiet_mode_switch_{nullptr}; #endif public: HonClimate(); @@ -95,6 +109,8 @@ class HonClimate : public HaierClimateBase { void dump_config() override; void set_beeper_state(bool state); bool get_beeper_state() const; + void set_quiet_mode_state(bool state); + bool get_quiet_mode_state() const; esphome::optional get_vertical_airflow() const; void set_vertical_airflow(hon_protocol::VerticalSwingMode direction); esphome::optional get_horizontal_airflow() const; @@ -153,7 +169,6 @@ class HonClimate : public HaierClimateBase { bool functions_[5]; }; - bool beeper_status_; CleaningState cleaning_status_; bool got_valid_outdoor_temp_; esphome::optional pending_vertical_direction_{}; @@ -175,7 +190,8 @@ class HonClimate : public HaierClimateBase { esphome::optional current_vertical_swing_{}; esphome::optional current_horizontal_swing_{}; HonSettings settings_; - ESPPreferenceObject rtc_; + ESPPreferenceObject hon_rtc_; + SwitchState quiet_mode_state_{SwitchState::OFF}; }; class HaierAlarmStartTrigger : public Trigger { diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index 028e8a4087..63c22821b3 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -376,8 +376,10 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { } } } - out_data->display_status = this->display_status_ ? 0 : 1; - out_data->health_mode = this->health_mode_ ? 1 : 0; + out_data->display_status = this->get_display_state() ? 0 : 1; + this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01); + out_data->health_mode = this->get_health_mode() ? 1 : 0; + this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01); return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, sizeof(smartair2_protocol::HaierPacketControl)); } @@ -446,28 +448,26 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin } should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); } - { - // Display status - // should be before "Climate mode" because it is changing this->mode - if (packet.control.ac_power != 0) { - // if AC is off display status always ON so process it only when AC is on - bool disp_status = packet.control.display_status == 0; - if (disp_status != this->display_status_) { - // Do something only if display status changed - if (this->mode == CLIMATE_MODE_OFF) { - // AC just turned on from remote need to turn off display - this->force_send_control_ = true; - } else { - this->display_status_ = disp_status; - } + // Display status + // should be before "Climate mode" because it is changing this->mode + if (packet.control.ac_power != 0) { + // if AC is off display status always ON so process it only when AC is on + bool disp_status = packet.control.display_status == 0; + if (disp_status != this->get_display_state()) { + // Do something only if display status changed + if (this->mode == CLIMATE_MODE_OFF) { + // AC just turned on from remote need to turn off display + this->force_send_control_ = true; + } else if ((((uint8_t) this->health_mode_) & 0b10) == 0) { + this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF; } } } - { - // Health mode - bool old_health_mode = this->health_mode_; - this->health_mode_ = packet.control.health_mode == 1; - should_publish = should_publish || (old_health_mode != this->health_mode_); + // Health mode + if ((((uint8_t) this->health_mode_) & 0b10) == 0) { + bool old_health_mode = this->get_health_mode(); + this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF; + should_publish = should_publish || (old_health_mode != this->get_health_mode()); } { // Climate mode diff --git a/esphome/components/haier/switch/__init__.py b/esphome/components/haier/switch/__init__.py new file mode 100644 index 0000000000..6076cb0bd5 --- /dev/null +++ b/esphome/components/haier/switch/__init__.py @@ -0,0 +1,91 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome.components import switch +from esphome.const import ( + CONF_BEEPER, + CONF_DISPLAY, + ENTITY_CATEGORY_CONFIG, +) +from ..climate import ( + CONF_HAIER_ID, + CONF_PROTOCOL, + HaierClimateBase, + haier_ns, + PROTOCOL_HON, +) + +CODEOWNERS = ["@paveldn"] +BeeperSwitch = haier_ns.class_("BeeperSwitch", switch.Switch) +HealthModeSwitch = haier_ns.class_("HealthModeSwitch", switch.Switch) +DisplaySwitch = haier_ns.class_("DisplaySwitch", switch.Switch) +QuietModeSwitch = haier_ns.class_("QuietModeSwitch", switch.Switch) + +# Haier switches +CONF_HEALTH_MODE = "health_mode" +CONF_QUIET_MODE = "quiet_mode" + +# Additional icons +ICON_LEAF = "mdi:leaf" +ICON_LED_ON = "mdi:led-on" +ICON_VOLUME_HIGH = "mdi:volume-high" +ICON_VOLUME_OFF = "mdi:volume-off" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HAIER_ID): cv.use_id(HaierClimateBase), + cv.Optional(CONF_DISPLAY): switch.switch_schema( + DisplaySwitch, + icon=ICON_LED_ON, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="DISABLED", + ), + cv.Optional(CONF_HEALTH_MODE): switch.switch_schema( + HealthModeSwitch, + icon=ICON_LEAF, + default_restore_mode="DISABLED", + ), + # Beeper switch is only supported for HonClimate + cv.Optional(CONF_BEEPER): switch.switch_schema( + BeeperSwitch, + icon=ICON_VOLUME_HIGH, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="DISABLED", + ), + # Quiet mode is only supported for HonClimate + cv.Optional(CONF_QUIET_MODE): switch.switch_schema( + QuietModeSwitch, + icon=ICON_VOLUME_OFF, + entity_category=ENTITY_CATEGORY_CONFIG, + default_restore_mode="DISABLED", + ), + } +) + + +def _final_validate(config): + full_config = fv.full_config.get() + for switch_type in [CONF_BEEPER, CONF_QUIET_MODE]: + # Check switches that are only supported for HonClimate + if config.get(switch_type): + climate_path = full_config.get_path_for_id(config[CONF_HAIER_ID])[:-1] + climate_conf = full_config.get_config_for_path(climate_path) + protocol_type = climate_conf.get(CONF_PROTOCOL) + if protocol_type.casefold() != PROTOCOL_HON.casefold(): + raise cv.Invalid( + f"{switch_type} switch is only supported for hon climate" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_HAIER_ID]) + + for switch_type in [CONF_DISPLAY, CONF_HEALTH_MODE, CONF_BEEPER, CONF_QUIET_MODE]: + if conf := config.get(switch_type): + sw_var = await switch.new_switch(conf) + await cg.register_parented(sw_var, parent) + cg.add(getattr(parent, f"set_{switch_type}_switch")(sw_var)) diff --git a/esphome/components/haier/switch/beeper.cpp b/esphome/components/haier/switch/beeper.cpp new file mode 100644 index 0000000000..1ce64d0848 --- /dev/null +++ b/esphome/components/haier/switch/beeper.cpp @@ -0,0 +1,14 @@ +#include "beeper.h" + +namespace esphome { +namespace haier { + +void BeeperSwitch::write_state(bool state) { + if (this->parent_->get_beeper_state() != state) { + this->parent_->set_beeper_state(state); + } + this->publish_state(state); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/beeper.h b/esphome/components/haier/switch/beeper.h new file mode 100644 index 0000000000..7396a7a0dd --- /dev/null +++ b/esphome/components/haier/switch/beeper.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../hon_climate.h" + +namespace esphome { +namespace haier { + +class BeeperSwitch : public switch_::Switch, public Parented { + public: + BeeperSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/display.cpp b/esphome/components/haier/switch/display.cpp new file mode 100644 index 0000000000..5e24843dcf --- /dev/null +++ b/esphome/components/haier/switch/display.cpp @@ -0,0 +1,14 @@ +#include "display.h" + +namespace esphome { +namespace haier { + +void DisplaySwitch::write_state(bool state) { + if (this->parent_->get_display_state() != state) { + this->parent_->set_display_state(state); + } + this->publish_state(state); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/display.h b/esphome/components/haier/switch/display.h new file mode 100644 index 0000000000..f93ccfcdb7 --- /dev/null +++ b/esphome/components/haier/switch/display.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../haier_base.h" + +namespace esphome { +namespace haier { + +class DisplaySwitch : public switch_::Switch, public Parented { + public: + DisplaySwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/health_mode.cpp b/esphome/components/haier/switch/health_mode.cpp new file mode 100644 index 0000000000..3715759bdd --- /dev/null +++ b/esphome/components/haier/switch/health_mode.cpp @@ -0,0 +1,14 @@ +#include "health_mode.h" + +namespace esphome { +namespace haier { + +void HealthModeSwitch::write_state(bool state) { + if (this->parent_->get_health_mode() != state) { + this->parent_->set_health_mode(state); + } + this->publish_state(state); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/health_mode.h b/esphome/components/haier/switch/health_mode.h new file mode 100644 index 0000000000..cfd2aa2f22 --- /dev/null +++ b/esphome/components/haier/switch/health_mode.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../haier_base.h" + +namespace esphome { +namespace haier { + +class HealthModeSwitch : public switch_::Switch, public Parented { + public: + HealthModeSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/quiet_mode.cpp b/esphome/components/haier/switch/quiet_mode.cpp new file mode 100644 index 0000000000..056312b5f0 --- /dev/null +++ b/esphome/components/haier/switch/quiet_mode.cpp @@ -0,0 +1,14 @@ +#include "quiet_mode.h" + +namespace esphome { +namespace haier { + +void QuietModeSwitch::write_state(bool state) { + if (this->parent_->get_quiet_mode_state() != state) { + this->parent_->set_quiet_mode_state(state); + } + this->publish_state(state); +} + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/haier/switch/quiet_mode.h b/esphome/components/haier/switch/quiet_mode.h new file mode 100644 index 0000000000..bad5289500 --- /dev/null +++ b/esphome/components/haier/switch/quiet_mode.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../hon_climate.h" + +namespace esphome { +namespace haier { + +class QuietModeSwitch : public switch_::Switch, public Parented { + public: + QuietModeSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace haier +} // namespace esphome diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 22a5af259d..bba886b39b 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -25,6 +25,7 @@ I2SAudioSpeaker = i2s_audio_ns.class_( CONF_DAC_TYPE = "dac_type" +CONF_I2S_COMM_FMT = "i2s_comm_fmt" i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") INTERNAL_DAC_OPTIONS = { @@ -33,6 +34,20 @@ INTERNAL_DAC_OPTIONS = { CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, } +i2s_comm_format_t = cg.global_ns.enum("i2s_comm_format_t") +I2C_COMM_FMT_OPTIONS = { + "stand_i2s": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_I2S, + "stand_msb": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_MSB, + "stand_pcm_short": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_PCM_SHORT, + "stand_pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_PCM_LONG, + "stand_max": i2s_comm_format_t.I2S_COMM_FORMAT_STAND_MAX, + "i2s_msb": i2s_comm_format_t.I2S_COMM_FORMAT_I2S_MSB, + "i2s_lsb": i2s_comm_format_t.I2S_COMM_FORMAT_I2S_LSB, + "pcm": i2s_comm_format_t.I2S_COMM_FORMAT_PCM, + "pcm_short": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_SHORT, + "pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_LONG, +} + NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] @@ -77,6 +92,9 @@ CONFIG_SCHEMA = cv.All( cv.Required( CONF_I2S_DOUT_PIN ): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_COMM_FMT, default="stand_i2s"): cv.enum( + I2C_COMM_FMT_OPTIONS, lower=True + ), } ), }, @@ -96,4 +114,5 @@ async def to_code(config): cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL])) else: cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) + cg.add(var.set_i2s_comm_fmt(config[CONF_I2S_COMM_FMT])) cg.add(var.set_timeout(config[CONF_TIMEOUT])) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 4b427898a2..97c1d86c36 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -83,7 +83,7 @@ void I2SAudioSpeaker::player_task(void *params) { .sample_rate = this_speaker->sample_rate_, .bits_per_sample = this_speaker->bits_per_sample_, .channel_format = this_speaker->channel_, - .communication_format = I2S_COMM_FORMAT_STAND_I2S, + .communication_format = this_speaker->i2s_comm_fmt_, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = 256, diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 7adc4e8a24..9d1817c86f 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -49,6 +49,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp #if SOC_I2S_SUPPORTS_DAC void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; } #endif + void set_i2s_comm_fmt(i2s_comm_format_t mode) { this->i2s_comm_fmt_ = mode; } void start() override; void stop() override; @@ -76,6 +77,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp #if SOC_I2S_SUPPORTS_DAC i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; #endif + i2s_comm_format_t i2s_comm_fmt_; }; } // namespace i2s_audio diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index 58a146d2fd..8fe7f7d41d 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -53,6 +53,8 @@ MODELS = { "inkplate_10": InkplateModel.INKPLATE_10, "inkplate_6_plus": InkplateModel.INKPLATE_6_PLUS, "inkplate_6_v2": InkplateModel.INKPLATE_6_V2, + "inkplate_5": InkplateModel.INKPLATE_5, + "inkplate_5_v2": InkplateModel.INKPLATE_5_V2, } CONFIG_SCHEMA = cv.All( diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate6/inkplate.h index 2946c89e1c..ca2ad46f1e 100644 --- a/esphome/components/inkplate6/inkplate.h +++ b/esphome/components/inkplate6/inkplate.h @@ -15,6 +15,8 @@ enum InkplateModel : uint8_t { INKPLATE_10 = 1, INKPLATE_6_PLUS = 2, INKPLATE_6_V2 = 3, + INKPLATE_5 = 4, + INKPLATE_5_V2 = 5, }; class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { @@ -29,7 +31,7 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { const uint8_t pixelMaskLUT[8] = {0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80}; const uint8_t pixelMaskGLUT[2] = {0x0F, 0xF0}; - const uint8_t waveform3BitAll[4][8][9] = {// INKPLATE_6 + const uint8_t waveform3BitAll[6][8][9] = {// INKPLATE_6 {{0, 1, 1, 0, 0, 1, 1, 0, 0}, {0, 1, 2, 1, 1, 2, 1, 0, 0}, {1, 1, 1, 2, 2, 1, 0, 0, 0}, @@ -64,7 +66,25 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { {1, 1, 1, 1, 2, 2, 1, 0, 0}, {0, 1, 1, 1, 2, 2, 1, 0, 0}, {0, 0, 0, 0, 1, 1, 2, 0, 0}, - {0, 0, 0, 0, 0, 1, 2, 0, 0}}}; + {0, 0, 0, 0, 0, 1, 2, 0, 0}}, + // INKPLATE_5 + {{0, 0, 1, 1, 0, 1, 1, 1, 0}, + {0, 1, 1, 1, 1, 2, 0, 1, 0}, + {1, 2, 2, 0, 2, 1, 1, 1, 0}, + {1, 1, 1, 2, 0, 1, 1, 2, 0}, + {0, 1, 1, 1, 2, 0, 1, 2, 0}, + {0, 0, 0, 1, 1, 2, 1, 2, 0}, + {1, 1, 1, 2, 0, 2, 1, 2, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0}}, + // INKPLATE_5_V2 + {{0, 0, 1, 1, 2, 1, 1, 1, 0}, + {1, 1, 2, 2, 1, 2, 1, 1, 0}, + {0, 1, 2, 2, 1, 1, 2, 1, 0}, + {0, 0, 1, 1, 1, 1, 1, 2, 0}, + {1, 2, 1, 2, 1, 1, 1, 2, 0}, + {0, 1, 1, 1, 2, 0, 1, 2, 0}, + {1, 1, 1, 2, 2, 2, 1, 2, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0}}}; void set_greyscale(bool greyscale) { this->greyscale_ = greyscale; @@ -146,6 +166,10 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { return 800; } else if (this->model_ == INKPLATE_10) { return 1200; + } else if (this->model_ == INKPLATE_5) { + return 960; + } else if (this->model_ == INKPLATE_5_V2) { + return 1280; } else if (this->model_ == INKPLATE_6_PLUS) { return 1024; } @@ -155,6 +179,10 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { int get_height_internal() override { if (this->model_ == INKPLATE_6 || this->model_ == INKPLATE_6_V2) { return 600; + } else if (this->model_ == INKPLATE_5) { + return 540; + } else if (this->model_ == INKPLATE_5_V2) { + return 720; } else if (this->model_ == INKPLATE_10) { return 825; } else if (this->model_ == INKPLATE_6_PLUS) { diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 64f254cde8..a3a6f7ddaf 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -22,9 +22,10 @@ from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid from .automation import disp_update, focused_widgets, update_to_code -from .defines import add_define +from .defines import CONF_WIDGETS, add_define from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code from .gradient import GRADIENT_SCHEMA, gradients_to_code +from .hello_world import get_hello_world from .lv_validation import lv_bool, lv_images_used from .lvcode import LvContext, LvglComponent from .schemas import ( @@ -32,7 +33,7 @@ from .schemas import ( FLEX_OBJ_SCHEMA, GRID_CELL_SCHEMA, LAYOUT_SCHEMAS, - STYLE_SCHEMA, + STATE_SCHEMA, WIDGET_TYPES, any_widget_schema, container_schema, @@ -292,6 +293,13 @@ def display_schema(config): return value or [cv.use_id(Display)(config)] +def add_hello_world(config): + if CONF_WIDGETS not in config and CONF_PAGES not in config: + LOGGER.info("No pages or widgets configured, creating default hello_world page") + config[CONF_WIDGETS] = cv.ensure_list(WIDGET_SCHEMA)(get_hello_world()) + return config + + FINAL_VALIDATE_SCHEMA = final_validation CONFIG_SCHEMA = ( @@ -313,7 +321,7 @@ CONFIG_SCHEMA = ( ), cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list( cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}) - .extend(STYLE_SCHEMA) + .extend(STATE_SCHEMA) .extend( { cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, @@ -349,4 +357,5 @@ CONFIG_SCHEMA = ( } ) .extend(DISP_BG_SCHEMA) -).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS)) + .add_extra(add_hello_world) +) diff --git a/esphome/components/lvgl/hello_world.py b/esphome/components/lvgl/hello_world.py new file mode 100644 index 0000000000..2c2ec6732c --- /dev/null +++ b/esphome/components/lvgl/hello_world.py @@ -0,0 +1,64 @@ +from io import StringIO + +from esphome.yaml_util import parse_yaml + +CONFIG = """ +- obj: + radius: 0 + pad_all: 12 + bg_color: 0xFFFFFF + height: 100% + width: 100% + widgets: + - spinner: + id: hello_world_spinner_ + align: center + indicator: + arc_color: tomato + height: 100 + width: 100 + spin_time: 2s + arc_length: 60deg + - label: + id: hello_world_label_ + text: "Hello World!" + align: center + on_click: + lvgl.spinner.update: + id: hello_world_spinner_ + arc_color: springgreen + - checkbox: + pad_all: 8 + text: Checkbox + align: top_right + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Checked!" + - button: + pad_all: 8 + checkable: true + align: top_left + text_font: montserrat_20 + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Clicked!" + widgets: + - label: + text: "Button" + - slider: + width: 80% + align: bottom_mid + on_value: + lvgl.label.update: + id: hello_world_label_ + text: + format: "%.0f%%" + args: [x] +""" + + +def get_hello_world(): + with StringIO(CONFIG) as fp: + return parse_yaml("hello_world", fp) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 8593deb869..3dee0189fb 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -49,17 +49,172 @@ def opacity_validator(value): opacity = LValidator(opacity_validator, uint32, retmapper=literal) +COLOR_NAMES = { + "aliceblue": 0xF0F8FF, + "antiquewhite": 0xFAEBD7, + "aqua": 0x00FFFF, + "aquamarine": 0x7FFFD4, + "azure": 0xF0FFFF, + "beige": 0xF5F5DC, + "bisque": 0xFFE4C4, + "black": 0x000000, + "blanchedalmond": 0xFFEBCD, + "blue": 0x0000FF, + "blueviolet": 0x8A2BE2, + "brown": 0xA52A2A, + "burlywood": 0xDEB887, + "cadetblue": 0x5F9EA0, + "chartreuse": 0x7FFF00, + "chocolate": 0xD2691E, + "coral": 0xFF7F50, + "cornflowerblue": 0x6495ED, + "cornsilk": 0xFFF8DC, + "crimson": 0xDC143C, + "cyan": 0x00FFFF, + "darkblue": 0x00008B, + "darkcyan": 0x008B8B, + "darkgoldenrod": 0xB8860B, + "darkgray": 0xA9A9A9, + "darkgreen": 0x006400, + "darkgrey": 0xA9A9A9, + "darkkhaki": 0xBDB76B, + "darkmagenta": 0x8B008B, + "darkolivegreen": 0x556B2F, + "darkorange": 0xFF8C00, + "darkorchid": 0x9932CC, + "darkred": 0x8B0000, + "darksalmon": 0xE9967A, + "darkseagreen": 0x8FBC8F, + "darkslateblue": 0x483D8B, + "darkslategray": 0x2F4F4F, + "darkslategrey": 0x2F4F4F, + "darkturquoise": 0x00CED1, + "darkviolet": 0x9400D3, + "deeppink": 0xFF1493, + "deepskyblue": 0x00BFFF, + "dimgray": 0x696969, + "dimgrey": 0x696969, + "dodgerblue": 0x1E90FF, + "firebrick": 0xB22222, + "floralwhite": 0xFFFAF0, + "forestgreen": 0x228B22, + "fuchsia": 0xFF00FF, + "gainsboro": 0xDCDCDC, + "ghostwhite": 0xF8F8FF, + "goldenrod": 0xDAA520, + "gold": 0xFFD700, + "gray": 0x808080, + "green": 0x008000, + "greenyellow": 0xADFF2F, + "grey": 0x808080, + "honeydew": 0xF0FFF0, + "hotpink": 0xFF69B4, + "indianred": 0xCD5C5C, + "indigo": 0x4B0082, + "ivory": 0xFFFFF0, + "khaki": 0xF0E68C, + "lavenderblush": 0xFFF0F5, + "lavender": 0xE6E6FA, + "lawngreen": 0x7CFC00, + "lemonchiffon": 0xFFFACD, + "lightblue": 0xADD8E6, + "lightcoral": 0xF08080, + "lightcyan": 0xE0FFFF, + "lightgoldenrodyellow": 0xFAFAD2, + "lightgray": 0xD3D3D3, + "lightgreen": 0x90EE90, + "lightgrey": 0xD3D3D3, + "lightpink": 0xFFB6C1, + "lightsalmon": 0xFFA07A, + "lightseagreen": 0x20B2AA, + "lightskyblue": 0x87CEFA, + "lightslategray": 0x778899, + "lightslategrey": 0x778899, + "lightsteelblue": 0xB0C4DE, + "lightyellow": 0xFFFFE0, + "lime": 0x00FF00, + "limegreen": 0x32CD32, + "linen": 0xFAF0E6, + "magenta": 0xFF00FF, + "maroon": 0x800000, + "mediumaquamarine": 0x66CDAA, + "mediumblue": 0x0000CD, + "mediumorchid": 0xBA55D3, + "mediumpurple": 0x9370DB, + "mediumseagreen": 0x3CB371, + "mediumslateblue": 0x7B68EE, + "mediumspringgreen": 0x00FA9A, + "mediumturquoise": 0x48D1CC, + "mediumvioletred": 0xC71585, + "midnightblue": 0x191970, + "mintcream": 0xF5FFFA, + "mistyrose": 0xFFE4E1, + "moccasin": 0xFFE4B5, + "navajowhite": 0xFFDEAD, + "navy": 0x000080, + "oldlace": 0xFDF5E6, + "olive": 0x808000, + "olivedrab": 0x6B8E23, + "orange": 0xFFA500, + "orangered": 0xFF4500, + "orchid": 0xDA70D6, + "palegoldenrod": 0xEEE8AA, + "palegreen": 0x98FB98, + "paleturquoise": 0xAFEEEE, + "palevioletred": 0xDB7093, + "papayawhip": 0xFFEFD5, + "peachpuff": 0xFFDAB9, + "peru": 0xCD853F, + "pink": 0xFFC0CB, + "plum": 0xDDA0DD, + "powderblue": 0xB0E0E6, + "purple": 0x800080, + "rebeccapurple": 0x663399, + "red": 0xFF0000, + "rosybrown": 0xBC8F8F, + "royalblue": 0x4169E1, + "saddlebrown": 0x8B4513, + "salmon": 0xFA8072, + "sandybrown": 0xF4A460, + "seagreen": 0x2E8B57, + "seashell": 0xFFF5EE, + "sienna": 0xA0522D, + "silver": 0xC0C0C0, + "skyblue": 0x87CEEB, + "slateblue": 0x6A5ACD, + "slategray": 0x708090, + "slategrey": 0x708090, + "snow": 0xFFFAFA, + "springgreen": 0x00FF7F, + "steelblue": 0x4682B4, + "tan": 0xD2B48C, + "teal": 0x008080, + "thistle": 0xD8BFD8, + "tomato": 0xFF6347, + "turquoise": 0x40E0D0, + "violet": 0xEE82EE, + "wheat": 0xF5DEB3, + "white": 0xFFFFFF, + "whitesmoke": 0xF5F5F5, + "yellow": 0xFFFF00, + "yellowgreen": 0x9ACD32, +} + @schema_extractor("one_of") def color(value): if value == SCHEMA_EXTRACT: return ["hex color value", "color ID"] - return cv.Any(cv.int_, cv.use_id(ColorStruct))(value) + return cv.Any(cv.int_, cv.one_of(*COLOR_NAMES, lower=True), cv.use_id(ColorStruct))( + value + ) def color_retmapper(value): if isinstance(value, cv.Lambda): return cv.returning_lambda(value) + if isinstance(value, str) and value in COLOR_NAMES: + value = COLOR_NAMES[value] if isinstance(value, int): return literal( f"lv_color_make({(value >> 16) & 0xFF}, {(value >> 8) & 0xFF}, {value & 0xFF})" diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index a8aa590951..0862406e46 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -419,6 +419,13 @@ async def to_code(config): repo="https://github.com/espressif/esp-tflite-micro", ref="v1.3.1", ) + # add esp-nn dependency for tflite-micro to work around https://github.com/espressif/esp-nn/issues/17 + # ...remove after switching to IDF 5.1.4+ + esp32.add_idf_component( + name="esp-nn", + repo="https://github.com/espressif/esp-nn", + ref="v1.1.0", + ) cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 240b407819..336d928f71 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_CERTIFICATE_AUTHORITY, + CONF_CLEAN_SESSION, CONF_CLIENT_CERTIFICATE, CONF_CLIENT_CERTIFICATE_KEY, CONF_CLIENT_ID, @@ -209,6 +210,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PORT, default=1883): cv.port, cv.Optional(CONF_USERNAME, default=""): cv.string, cv.Optional(CONF_PASSWORD, default=""): cv.string, + cv.Optional(CONF_CLEAN_SESSION, default=False): cv.boolean, cv.Optional(CONF_CLIENT_ID): cv.string, cv.SplitDefault(CONF_IDF_SEND_ASYNC, esp32_idf=False): cv.All( cv.boolean, cv.only_with_esp_idf @@ -325,6 +327,7 @@ async def to_code(config): cg.add(var.set_broker_port(config[CONF_PORT])) cg.add(var.set_username(config[CONF_USERNAME])) cg.add(var.set_password(config[CONF_PASSWORD])) + cg.add(var.set_clean_session(config[CONF_CLEAN_SESSION])) if CONF_CLIENT_ID in config: cg.add(var.set_client_id(config[CONF_CLIENT_ID])) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index c19b24c0cf..b5ac285026 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -147,6 +147,7 @@ void MQTTClientComponent::dump_config() { this->ip_.str().c_str()); ESP_LOGCONFIG(TAG, " Username: " LOG_SECRET("'%s'"), this->credentials_.username.c_str()); ESP_LOGCONFIG(TAG, " Client ID: " LOG_SECRET("'%s'"), this->credentials_.client_id.c_str()); + ESP_LOGCONFIG(TAG, " Clean Session: %s", YESNO(this->credentials_.clean_session)); if (this->is_discovery_ip_enabled()) { ESP_LOGCONFIG(TAG, " Discovery IP enabled"); } @@ -246,6 +247,7 @@ void MQTTClientComponent::start_connect_() { this->mqtt_backend_.disconnect(); this->mqtt_backend_.set_client_id(this->credentials_.client_id.c_str()); + this->mqtt_backend_.set_clean_session(this->credentials_.clean_session); const char *username = nullptr; if (!this->credentials_.username.empty()) username = this->credentials_.username.c_str(); diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index b0d3bbe66d..887800f201 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -51,6 +51,7 @@ struct MQTTCredentials { std::string username; std::string password; std::string client_id; ///< The client ID. Will automatically be truncated to 23 characters. + bool clean_session; ///< Whether the session will be cleaned or remembered between connects. }; /// Simple data struct for Home Assistant component availability. @@ -254,6 +255,7 @@ class MQTTClientComponent : public Component { void set_username(const std::string &username) { this->credentials_.username = username; } void set_password(const std::string &password) { this->credentials_.password = password; } void set_client_id(const std::string &client_id) { this->credentials_.client_id = client_id; } + void set_clean_session(const bool &clean_session) { this->credentials_.clean_session = clean_session; } void set_on_connect(mqtt_on_connect_callback_t &&callback); void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback); diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index d12434ec8f..2924f66d3c 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -27,6 +27,7 @@ CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" CONF_FONT_ID = "font_id" CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" +CONF_SKIP_CONNECTION_HANDSHAKE = "skip_connection_handshake" def NextionName(value): diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index ce45d25e7b..e403ba7ae8 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -23,6 +23,7 @@ from .base_component import ( CONF_START_UP_PAGE, CONF_AUTO_WAKE_ON_TOUCH, CONF_EXIT_REPARSE_ON_START, + CONF_SKIP_CONNECTION_HANDSHAKE, ) CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] @@ -72,6 +73,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_START_UP_PAGE): cv.uint8_t, cv.Optional(CONF_AUTO_WAKE_ON_TOUCH, default=True): cv.boolean, cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean, + cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("5s")) @@ -118,6 +120,8 @@ async def to_code(config): cg.add(var.set_exit_reparse_on_start_internal(config[CONF_EXIT_REPARSE_ON_START])) + cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE])) + await display.register_display(var, config) for conf in config.get(CONF_ON_SETUP, []): diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index ddbd3328ef..a80f6efc91 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -43,6 +43,16 @@ bool Nextion::check_connect_() { if (this->get_is_connected_()) return true; + // Check if the handshake should be skipped for the Nextion connection + if (this->skip_connection_handshake_) { + // Log the connection status without handshake + ESP_LOGW(TAG, "Nextion display set as connected without performing handshake"); + // Set the connection status to true + this->is_connected_ = true; + // Return true indicating the connection is set + return true; + } + if (this->comok_sent_ == 0) { this->reset_(false); @@ -126,10 +136,14 @@ void Nextion::reset_(bool reset_nextion) { void Nextion::dump_config() { ESP_LOGCONFIG(TAG, "Nextion:"); - ESP_LOGCONFIG(TAG, " Device Model: %s", this->device_model_.c_str()); - ESP_LOGCONFIG(TAG, " Firmware Version: %s", this->firmware_version_.c_str()); - ESP_LOGCONFIG(TAG, " Serial Number: %s", this->serial_number_.c_str()); - ESP_LOGCONFIG(TAG, " Flash Size: %s", this->flash_size_.c_str()); + if (this->skip_connection_handshake_) { + ESP_LOGCONFIG(TAG, " Skip handshake: %s", YESNO(this->skip_connection_handshake_)); + } else { + ESP_LOGCONFIG(TAG, " Device Model: %s", this->device_model_.c_str()); + ESP_LOGCONFIG(TAG, " Firmware Version: %s", this->firmware_version_.c_str()); + ESP_LOGCONFIG(TAG, " Serial Number: %s", this->serial_number_.c_str()); + ESP_LOGCONFIG(TAG, " Flash Size: %s", this->flash_size_.c_str()); + } ESP_LOGCONFIG(TAG, " Wake On Touch: %s", YESNO(this->auto_wake_on_touch_)); ESP_LOGCONFIG(TAG, " Exit reparse: %s", YESNO(this->exit_reparse_on_start_)); @@ -262,6 +276,7 @@ void Nextion::loop() { this->goto_page(this->start_up_page_); } + // This could probably be removed from the loop area, as those are redundant. this->set_auto_wake_on_touch(this->auto_wake_on_touch_); this->set_exit_reparse_on_start(this->exit_reparse_on_start_); diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 4546baa4d8..732ee9b455 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -926,6 +926,21 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void set_exit_reparse_on_start(bool exit_reparse); + /** + * Sets whether the Nextion display should skip the connection handshake process. + * @param skip_handshake True or false. When skip_connection_handshake is true, + * the connection will be established without performing the handshake. + * This can be useful when using Nextion Simulator. + * + * Example: + * ```cpp + * it.set_skip_connection_handshake(true); + * ``` + * + * When set to true, the display will be marked as connected without performing a handshake. + */ + void set_skip_connection_handshake(bool skip_handshake) { this->skip_connection_handshake_ = skip_handshake; } + /** * Sets Nextion mode between sleep and awake * @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode. @@ -1221,6 +1236,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe int16_t start_up_page_ = -1; bool auto_wake_on_touch_ = true; bool exit_reparse_on_start_ = false; + bool skip_connection_handshake_ = false; /** * Manually send a raw command to the display and don't wait for an acknowledgement packet. diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index d203ff3417..f979939739 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -1,10 +1,14 @@ +from esphome import automation, pins import esphome.codegen as cg +from esphome.components import esp32_rmt, remote_base import esphome.config_validation as cv -from esphome import pins -from esphome.components import remote_base, esp32_rmt from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_ID, CONF_PIN, CONF_RMT_CHANNEL AUTO_LOAD = ["remote_base"] + +CONF_ON_TRANSMIT = "on_transmit" +CONF_ON_COMPLETE = "on_complete" + remote_transmitter_ns = cg.esphome_ns.namespace("remote_transmitter") RemoteTransmitterComponent = remote_transmitter_ns.class_( "RemoteTransmitterComponent", remote_base.RemoteTransmitterBase, cg.Component @@ -19,6 +23,8 @@ CONFIG_SCHEMA = cv.Schema( cv.percentage_int, cv.Range(min=1, max=100) ), cv.Optional(CONF_RMT_CHANNEL): esp32_rmt.validate_rmt_channel(tx=True), + cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), } ).extend(cv.COMPONENT_SCHEMA) @@ -32,3 +38,13 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_carrier_duty_percent(config[CONF_CARRIER_DUTY_PERCENT])) + + if on_transmit_config := config.get(CONF_ON_TRANSMIT): + await automation.build_automation( + var.get_transmit_trigger(), [], on_transmit_config + ) + + if on_complete_config := config.get(CONF_ON_COMPLETE): + await automation.build_automation( + var.get_complete_trigger(), [], on_complete_config + ) diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index a5896796c0..4abe687d23 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -33,6 +33,9 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; } + Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; + Trigger<> *get_complete_trigger() const { return this->complete_trigger_; }; + protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; #if defined(USE_ESP8266) || defined(USE_LIBRETINY) @@ -57,6 +60,9 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, bool inverted_{false}; #endif uint8_t carrier_duty_percent_; + + Trigger<> *transmit_trigger_{new Trigger<>()}; + Trigger<> *complete_trigger_{new Trigger<>()}; }; } // namespace remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index eea35019ff..bce2408723 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -124,6 +124,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen ESP_LOGE(TAG, "Empty data"); return; } + this->transmit_trigger_->trigger(); for (uint32_t i = 0; i < send_times; i++) { esp_err_t error = rmt_write_items(this->channel_, this->rmt_temp_.data(), this->rmt_temp_.size(), true); if (error != ESP_OK) { @@ -135,6 +136,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) delayMicroseconds(send_wait); } + this->complete_trigger_->trigger(); } } // namespace remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp index 1c0eb94e61..613f00b7f5 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp @@ -76,6 +76,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen uint32_t on_time, off_time; this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); this->target_time_ = 0; + this->transmit_trigger_->trigger(); for (uint32_t i = 0; i < send_times; i++) { for (int32_t item : this->temp_.get_data()) { if (item > 0) { @@ -93,6 +94,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) this->target_time_ += send_wait; } + this->complete_trigger_->trigger(); } } // namespace remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp b/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp index 78bb280482..ad9265fb14 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp @@ -78,6 +78,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen uint32_t on_time, off_time; this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); this->target_time_ = 0; + this->transmit_trigger_->trigger(); for (uint32_t i = 0; i < send_times; i++) { InterruptLock lock; for (int32_t item : this->temp_.get_data()) { @@ -96,6 +97,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) this->target_time_ += send_wait; } + this->complete_trigger_->trigger(); } } // namespace remote_transmitter diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 7248bc044e..403bff789d 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -9,14 +9,6 @@ void ST7701S::setup() { esph_log_config(TAG, "Setting up ST7701S"); this->spi_setup(); this->write_init_sequence_(); -} - -// called after a delay after writing the init sequence -void ST7701S::complete_setup_() { - this->write_command_(SLEEP_OUT); - this->write_command_(DISPLAY_ON); - this->spi_teardown(); // SPI not needed after this - delay(10); esp_lcd_rgb_panel_config_t config{}; config.flags.fb_in_psram = 1; @@ -179,7 +171,12 @@ void ST7701S::write_init_sequence_() { this->write_data_(val); ESP_LOGD(TAG, "write MADCTL %X", val); this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); - this->set_timeout(120, [this] { this->complete_setup_(); }); + // can't avoid this inline delay due to the need to complete setup before anything else tries to draw. + delay(120); // NOLINT + this->write_command_(SLEEP_OUT); + this->write_command_(DISPLAY_ON); + this->spi_teardown(); // SPI not needed after this + delay(10); } void ST7701S::dump_config() { diff --git a/esphome/components/tca9555/__init__.py b/esphome/components/tca9555/__init__.py new file mode 100644 index 0000000000..db0451d4e6 --- /dev/null +++ b/esphome/components/tca9555/__init__.py @@ -0,0 +1,72 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, +) + +CODEOWNERS = ["@mobrembski"] + +AUTO_LOAD = ["gpio_expander"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +tca9555_ns = cg.esphome_ns.namespace("tca9555") + +TCA9555Component = tca9555_ns.class_("TCA9555Component", cg.Component, i2c.I2CDevice) +TCA9555GPIOPin = tca9555_ns.class_("TCA9555GPIOPin", cg.GPIOPin) + +CONF_TCA9555 = "tca9555" +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(TCA9555Component), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x21)) +) + + +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 + + +TCA9555_PIN_SCHEMA = pins.gpio_base_schema( + TCA9555GPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, + invertable=True, +).extend( + { + cv.Required(CONF_TCA9555): cv.use_id(TCA9555Component), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_TCA9555, TCA9555_PIN_SCHEMA) +async def tca9555_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_TCA9555]) + + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp new file mode 100644 index 0000000000..cf0894427f --- /dev/null +++ b/esphome/components/tca9555/tca9555.cpp @@ -0,0 +1,140 @@ +#include "tca9555.h" +#include "esphome/core/log.h" + +static const uint8_t TCA9555_INPUT_PORT_REGISTER_0 = 0x00; +static const uint8_t TCA9555_INPUT_PORT_REGISTER_1 = 0x01; +static const uint8_t TCA9555_OUTPUT_PORT_REGISTER_0 = 0x02; +static const uint8_t TCA9555_OUTPUT_PORT_REGISTER_1 = 0x03; +static const uint8_t TCA9555_POLARITY_REGISTER_0 = 0x04; +static const uint8_t TCA9555_POLARITY_REGISTER_1 = 0x05; +static const uint8_t TCA9555_CONFIGURATION_PORT_0 = 0x06; +static const uint8_t TCA9555_CONFIGURATION_PORT_1 = 0x07; + +namespace esphome { +namespace tca9555 { + +static const char *const TAG = "tca9555"; + +void TCA9555Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up TCA9555..."); + if (!this->read_gpio_modes_()) { + this->mark_failed(); + return; + } + if (!this->read_gpio_outputs_()) { + this->mark_failed(); + return; + } +} +void TCA9555Component::dump_config() { + ESP_LOGCONFIG(TAG, "TCA9555:"); + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with TCA9555 failed!"); + } +} +void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags == gpio::FLAG_INPUT) { + // Set mode mask bit + this->mode_mask_ |= 1 << pin; + } else if (flags == gpio::FLAG_OUTPUT) { + // Clear mode mask bit + this->mode_mask_ &= ~(1 << pin); + } + // Write GPIO to enable input mode + this->write_gpio_modes_(); +} +void TCA9555Component::loop() { this->reset_pin_cache_(); } + +bool TCA9555Component::read_gpio_outputs_() { + if (this->is_failed()) + return false; + uint8_t data[2]; + if (!this->read_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { + this->status_set_warning("Failed to read output register"); + return false; + } + this->output_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); + this->status_clear_warning(); + return true; +} + +bool TCA9555Component::read_gpio_modes_() { + if (this->is_failed()) + return false; + uint8_t data[2]; + bool success = this->read_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2); + if (!success) { + this->status_set_warning("Failed to read mode register"); + return false; + } + this->mode_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); + + this->status_clear_warning(); + return true; +} +bool TCA9555Component::digital_read_hw(uint8_t pin) { + if (this->is_failed()) + return false; + bool success; + uint8_t data[2]; + success = this->read_bytes(TCA9555_INPUT_PORT_REGISTER_0, data, 2); + this->input_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); + + if (!success) { + this->status_set_warning("Failed to read input register"); + return false; + } + + this->status_clear_warning(); + return true; +} + +void TCA9555Component::digital_write_hw(uint8_t pin, bool value) { + if (this->is_failed()) + return; + + if (value) { + this->output_mask_ |= (1 << pin); + } else { + this->output_mask_ &= ~(1 << pin); + } + + uint8_t data[2]; + data[0] = this->output_mask_; + data[1] = this->output_mask_ >> 8; + if (!this->write_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { + this->status_set_warning("Failed to write output register"); + return; + } + + this->status_clear_warning(); +} + +bool TCA9555Component::write_gpio_modes_() { + if (this->is_failed()) + return false; + uint8_t data[2]; + + data[0] = this->mode_mask_; + data[1] = this->mode_mask_ >> 8; + if (!this->write_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2)) { + this->status_set_warning("Failed to write mode register"); + return false; + } + this->status_clear_warning(); + return true; +} + +bool TCA9555Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + +float TCA9555Component::get_setup_priority() const { return setup_priority::IO; } + +void TCA9555GPIOPin::setup() { this->pin_mode(this->flags_); } +void TCA9555GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool TCA9555GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void TCA9555GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string TCA9555GPIOPin::dump_summary() const { return str_sprintf("%u via TCA9555", this->pin_); } + +} // namespace tca9555 +} // namespace esphome diff --git a/esphome/components/tca9555/tca9555.h b/esphome/components/tca9555/tca9555.h new file mode 100644 index 0000000000..ea464db043 --- /dev/null +++ b/esphome/components/tca9555/tca9555.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tca9555 { + +class TCA9555Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { + public: + TCA9555Component() = default; + + /// Check i2c availability and setup masks + void setup() override; + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + + void dump_config() override; + + void loop() override; + + protected: + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + + /// Mask for the pin mode - 1 means output, 0 means input + uint16_t mode_mask_{0x00}; + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint16_t output_mask_{0x00}; + /// The state read in digital_read_hw - 1 means HIGH, 0 means LOW + uint16_t input_mask_{0x00}; + + bool read_gpio_modes_(); + bool write_gpio_modes_(); + bool read_gpio_outputs_(); +}; + +/// Helper class to expose a TCA9555 pin as an internal input GPIO pin. +class TCA9555GPIOPin : public GPIOPin, public Parented { + 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_pin(uint8_t pin) { this->pin_ = pin; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } + + protected: + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace tca9555 +} // namespace esphome diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 88c59eb761..c9b2ae321a 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -1,6 +1,7 @@ #include "tcs34725.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include namespace esphome { namespace tcs34725 { @@ -211,7 +212,7 @@ void TCS34725Component::update() { if (raw_c == 0) { channel_c = channel_r = channel_g = channel_b = 0.0f; } else { - float max_count = this->integration_time_ * 1024.0f / 2.4; + float max_count = this->integration_time_ <= 153.6f ? this->integration_time_ * 1024.0f / 2.4f : 65535.0f; float sum = raw_c; channel_r = raw_r / sum * 100.0f; channel_g = raw_g / sum * 100.0f; @@ -254,7 +255,8 @@ void TCS34725Component::update() { // change integration time an gain to achieve maximum resolution an dynamic range // calculate optimal integration time to achieve 70% satuaration float integration_time_ideal; - integration_time_ideal = 60 / ((float) raw_c / 655.35) * this->integration_time_; + + integration_time_ideal = 60 / ((float) std::max((uint16_t) 1, raw_c) / 655.35f) * this->integration_time_; uint8_t gain_reg_val_new = this->gain_reg_; // increase gain if less than 20% of white channel used and high integration time diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1bb7c6c249..3bb7eee8f1 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1012,6 +1012,16 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url call.set_mode(mode.c_str()); } + if (request->hasParam("fan_mode")) { + auto mode = request->getParam("fan_mode")->value(); + call.set_fan_mode(mode.c_str()); + } + + if (request->hasParam("swing_mode")) { + auto mode = request->getParam("swing_mode")->value(); + call.set_swing_mode(mode.c_str()); + } + if (request->hasParam("target_temperature_high")) { auto target_temperature_high = parse_number(request->getParam("target_temperature_high")->value().c_str()); if (target_temperature_high.has_value()) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 6008acb95d..c430d160f2 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -130,11 +130,16 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi } void WiFiComponent::wifi_pre_setup_() { -#ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC uint8_t mac[6]; +#ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC get_mac_address_raw(mac); set_mac_address(mac); ESP_LOGV(TAG, "Use EFuse MAC without checking CRC: %s", get_mac_address_pretty().c_str()); +#else + if (has_custom_mac_address()) { + get_mac_address_raw(mac); + set_mac_address(mac); + } #endif esp_err_t err = esp_netif_init(); if (err != ERR_OK) { diff --git a/esphome/const.py b/esphome/const.py index 6e7bbdec98..40b7a1c419 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -120,6 +120,7 @@ CONF_CHANNELS = "channels" CONF_CHARACTERISTIC_UUID = "characteristic_uuid" CONF_CHECK = "check" CONF_CHIPSET = "chipset" +CONF_CLEAN_SESSION = "clean_session" CONF_CLEAR_IMPEDANCE = "clear_impedance" CONF_CLIENT_CERTIFICATE = "client_certificate" CONF_CLIENT_CERTIFICATE_KEY = "client_certificate_key" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ffd5cc6f1b..bf676107c7 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -29,6 +29,7 @@ #define USE_DATETIME_TIME #define USE_DEEP_SLEEP #define USE_DISPLAY +#define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_EVENT #define USE_FAN #define USE_GRAPH @@ -45,10 +46,10 @@ #define USE_LVGL_BUTTONMATRIX #define USE_LVGL_FONT #define USE_LVGL_IMAGE -#define USE_LVGL_KEYBOARD #define USE_LVGL_KEY_LISTENER -#define USE_LVGL_TOUCHSCREEN +#define USE_LVGL_KEYBOARD #define USE_LVGL_ROTARY_ENCODER +#define USE_LVGL_TOUCHSCREEN #define USE_MDNS #define USE_MEDIA_PLAYER #define USE_MQTT diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index e75b06ccd3..2e99b0df70 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -671,9 +671,17 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame // match the CRC that goes along with it. For those devices, this // work-around reads and uses the MAC address as-is from EFuse, // without doing the CRC check. - esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); + if (has_custom_mac_address()) { + esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48); + } else { + esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); + } #else - esp_efuse_mac_get_default(mac); + if (has_custom_mac_address()) { + esp_efuse_mac_get_custom(mac); + } else { + esp_efuse_mac_get_default(mac); + } #endif #elif defined(USE_ESP8266) wifi_get_macaddr(STATION_IF, mac); @@ -685,20 +693,53 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame // this should be an error, but that messes with CI checks. #error No mac address method defined #endif } + std::string get_mac_address() { uint8_t mac[6]; get_mac_address_raw(mac); return str_snprintf("%02x%02x%02x%02x%02x%02x", 12, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); } + std::string get_mac_address_pretty() { uint8_t mac[6]; get_mac_address_raw(mac); return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); } + #ifdef USE_ESP32 void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } #endif +bool has_custom_mac_address() { +#ifdef USE_ESP32 + uint8_t mac[6]; +#if defined(CONFIG_SOC_IEEE802154_SUPPORTED) || defined(USE_ESP32_IGNORE_EFUSE_MAC_CRC) + return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); +#else + return (esp_efuse_mac_get_custom(mac) == ESP_OK) && mac_address_is_valid(mac); +#endif +#else + return false; +#endif +} + +bool mac_address_is_valid(const uint8_t *mac) { + bool is_all_zeros = true; + bool is_all_ones = true; + + for (uint8_t i = 0; i < 6; i++) { + if (mac[i] != 0) { + is_all_zeros = false; + } + } + for (uint8_t i = 0; i < 6; i++) { + if (mac[i] != 0xFF) { + is_all_ones = false; + } + } + return !(is_all_zeros || is_all_ones); +} + void delay_microseconds_safe(uint32_t us) { // avoids CPU locks that could trigger WDT or affect WiFi/BT stability uint32_t start = micros(); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 3e6fe9433e..7df4b84230 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -635,6 +635,14 @@ std::string get_mac_address_pretty(); void set_mac_address(uint8_t *mac); #endif +/// Check if a custom MAC address is set (ESP32 & variants) +/// @return True if a custom MAC address is set (ESP32 & variants), else false +bool has_custom_mac_address(); + +/// Check if the MAC address is not all zeros or all ones +/// @return True if MAC is valid, else false +bool mac_address_is_valid(const uint8_t *mac); + /// Delay for the given amount of microseconds, possibly yielding to other processes during the wait. void delay_microseconds_safe(uint32_t us); diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index d8ca831de0..f97c686684 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -11,16 +11,26 @@ namespace esphome { static const char *const TAG = "ring_buffer"; +RingBuffer::~RingBuffer() { + if (this->handle_ != nullptr) { + vStreamBufferDelete(this->handle_); + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + allocator.deallocate(this->storage_, this->size_); + } +} + std::unique_ptr RingBuffer::create(size_t len) { std::unique_ptr rb = make_unique(); + rb->size_ = len + 1; + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - rb->storage_ = allocator.allocate(len + 1); + rb->storage_ = allocator.allocate(rb->size_); if (rb->storage_ == nullptr) { return nullptr; } - rb->handle_ = xStreamBufferCreateStatic(len + 1, 1, rb->storage_, &rb->structure_); + rb->handle_ = xStreamBufferCreateStatic(rb->size_, 1, rb->storage_, &rb->structure_); ESP_LOGD(TAG, "Created ring buffer with size %u", len); return rb; } diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index 97ffefcefa..c0511fb52e 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -12,6 +12,8 @@ namespace esphome { class RingBuffer { public: + ~RingBuffer(); + /** * @brief Reads from the ring buffer, waiting up to a specified number of ticks if necessary. * @@ -83,6 +85,7 @@ class RingBuffer { StreamBufferHandle_t handle_; StaticStreamBuffer_t structure_; uint8_t *storage_; + size_t size_{0}; }; } // namespace esphome diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 7f1573b443..15f9206f21 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -226,4 +226,6 @@ class _Schema(vol.Schema): if isinstance(schema, vol.Schema): schema = schema.schema ret = super().extend(schema, extra=extra) - return _Schema(ret.schema, extra=ret.extra, extra_schemas=self._extra_schemas) + return _Schema( + ret.schema, extra=ret.extra, extra_schemas=self._extra_schemas.copy() + ) diff --git a/tests/components/bl0906/common.yaml b/tests/components/bl0906/common.yaml index 944791369c..29321a9471 100644 --- a/tests/components/bl0906/common.yaml +++ b/tests/components/bl0906/common.yaml @@ -8,6 +8,7 @@ uart: sensor: - platform: bl0906 + id: bl frequency: name: 'Frequency' temperature: @@ -60,3 +61,9 @@ sensor: name: 'Total_Energy' total_power: name: 'Total_Power' + +button: + - platform: template + id: reset + on_press: + - bl0906.reset_energy: bl diff --git a/tests/components/ch422g/common.yaml b/tests/components/ch422g/common.yaml index 02061bda59..d65956ecac 100644 --- a/tests/components/ch422g/common.yaml +++ b/tests/components/ch422g/common.yaml @@ -1,6 +1,5 @@ ch422g: - id: ch422g_hub - address: 0x24 binary_sensor: - platform: gpio @@ -11,10 +10,18 @@ binary_sensor: number: 1 mode: INPUT inverted: true +output: - platform: gpio - id: ch422g_output + id: ch422_out_0 pin: ch422g: ch422g_hub number: 0 mode: OUTPUT inverted: false + - platform: gpio + id: ch422_out_11 + pin: + ch422g: ch422g_hub + number: 11 + mode: OUTPUT_OPEN_DRAIN + inverted: true diff --git a/tests/components/ethernet/common-openeth.yaml b/tests/components/ethernet/common-openeth.yaml new file mode 100644 index 0000000000..fbb7579598 --- /dev/null +++ b/tests/components/ethernet/common-openeth.yaml @@ -0,0 +1,2 @@ +ethernet: + type: OPENETH diff --git a/tests/components/ethernet/test-openeth.esp32-idf.yaml b/tests/components/ethernet/test-openeth.esp32-idf.yaml new file mode 100644 index 0000000000..220316f3ee --- /dev/null +++ b/tests/components/ethernet/test-openeth.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-openeth.yaml diff --git a/tests/components/haier/common.yaml b/tests/components/haier/common.yaml index b8a23bac5a..368b88b69c 100644 --- a/tests/components/haier/common.yaml +++ b/tests/components/haier/common.yaml @@ -16,7 +16,6 @@ climate: name: Haier AC wifi_signal: true answer_timeout: 200ms - beeper: true visual: min_temperature: 16 °C max_temperature: 30 °C @@ -38,7 +37,6 @@ climate: supported_presets: - AWAY - BOOST - - ECO - SLEEP on_alarm_start: then: @@ -112,3 +110,15 @@ text_sensor: name: Haier cleaning status protocol_version: name: Haier protocol version + +switch: + - platform: haier + haier_id: haier_ac + beeper: + name: Haier beeper + display: + name: Haier display + health_mode: + name: Haier health mode + quiet_mode: + name: Haier quiet mode diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 9d157ea5b0..a3ed3047be 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -38,8 +38,8 @@ lvgl: border_width: 0 radius: 0 pad_all: 0 - border_color: 0x0077b3 - text_color: 0xFFFFFF + border_color: tomato + text_color: springgreen width: 100% height: 30 border_side: [left, top] diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml new file mode 100644 index 0000000000..3a490bbe15 --- /dev/null +++ b/tests/components/lvgl/test.host.yaml @@ -0,0 +1,11 @@ +display: + - platform: sdl + auto_clear_enabled: false + dimensions: + width: 480 + height: 480 + +touchscreen: + - platform: sdl + +lvgl: diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index b7d1655ec9..f7a727ab2f 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -10,6 +10,7 @@ mqtt: port: 1883 username: debug password: debug + clean_session: True client_id: someclient use_abbreviations: false discovery: true diff --git a/tests/components/tca9555/test.esp32-ard.yaml b/tests/components/tca9555/test.esp32-ard.yaml new file mode 100644 index 0000000000..e0c046b443 --- /dev/null +++ b/tests/components/tca9555/test.esp32-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 16 + sda: 17 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tca9555/test.esp32-c3-ard.yaml b/tests/components/tca9555/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..5c49b27640 --- /dev/null +++ b/tests/components/tca9555/test.esp32-c3-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 5 + sda: 4 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tca9555/test.esp32-c3-idf.yaml b/tests/components/tca9555/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..5c49b27640 --- /dev/null +++ b/tests/components/tca9555/test.esp32-c3-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 5 + sda: 4 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tca9555/test.esp32-idf.yaml b/tests/components/tca9555/test.esp32-idf.yaml new file mode 100644 index 0000000000..e0c046b443 --- /dev/null +++ b/tests/components/tca9555/test.esp32-idf.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 16 + sda: 17 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tca9555/test.esp8266-ard.yaml b/tests/components/tca9555/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5c49b27640 --- /dev/null +++ b/tests/components/tca9555/test.esp8266-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 5 + sda: 4 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tca9555/test.rp2040-ard.yaml b/tests/components/tca9555/test.rp2040-ard.yaml new file mode 100644 index 0000000000..5c49b27640 --- /dev/null +++ b/tests/components/tca9555/test.rp2040-ard.yaml @@ -0,0 +1,27 @@ +i2c: + - id: i2c_tca9555 + scl: 5 + sda: 4 + +tca9555: + - id: tca9555_hub + address: 0x21 + +binary_sensor: + - platform: gpio + id: tca9555_binary_sensor + name: TCA9555 Binary Sensor + pin: + tca9555: tca9555_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tca9555_output + pin: + tca9555: tca9555_hub + number: 0 + mode: OUTPUT + inverted: false