diff --git a/CODEOWNERS b/CODEOWNERS index 00a22fed7c..b7675f9406 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -104,6 +104,7 @@ esphome/components/cc1101/* @gabest11 @lygris esphome/components/ccs811/* @habbie esphome/components/cd74hc4067/* @asoehlke esphome/components/ch422g/* @clydebarrow @jesterret +esphome/components/ch423/* @dwmw2 esphome/components/chsc6x/* @kkosik20 esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet diff --git a/esphome/components/ch423/__init__.py b/esphome/components/ch423/__init__.py new file mode 100644 index 0000000000..e3990ee631 --- /dev/null +++ b/esphome/components/ch423/__init__.py @@ -0,0 +1,103 @@ +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, +) +from esphome.core import CORE + +CODEOWNERS = ["@dwmw2"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True +ch423_ns = cg.esphome_ns.namespace("ch423") + +CH423Component = ch423_ns.class_("CH423Component", cg.Component, i2c.I2CDevice) +CH423GPIOPin = ch423_ns.class_( + "CH423GPIOPin", cg.GPIOPin, cg.Parented.template(CH423Component) +) + +CONF_CH423 = "ch423" + +# Note that no address is configurable - each register in the CH423 has a dedicated i2c address +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(CH423Component), + 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]) + await cg.register_component(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("CH423 only supports input on pins 0-7") + if pin_config[CONF_MODE][CONF_OPEN_DRAIN] and pin_config[CONF_NUMBER] < 8: + raise cv.Invalid("CH423 only supports open drain output on pins 8-23") + + ch423_id = pin_config[CONF_CH423] + pin_num = pin_config[CONF_NUMBER] + is_output = pin_config[CONF_MODE][CONF_OUTPUT] + is_open_drain = pin_config[CONF_MODE][CONF_OPEN_DRAIN] + + # Track pin modes per CH423 instance in CORE.data + ch423_modes = CORE.data.setdefault(CONF_CH423, {}) + if ch423_id not in ch423_modes: + ch423_modes[ch423_id] = {"gpio_output": None, "gpo_open_drain": None} + + if pin_num < 8: + # GPIO pins (0-7): all must have same direction + if ch423_modes[ch423_id]["gpio_output"] is None: + ch423_modes[ch423_id]["gpio_output"] = is_output + elif ch423_modes[ch423_id]["gpio_output"] != is_output: + raise cv.Invalid( + "CH423 GPIO pins (0-7) must all be configured as input or all as output" + ) + # GPO pins (8-23): all must have same open-drain setting + elif ch423_modes[ch423_id]["gpo_open_drain"] is None: + ch423_modes[ch423_id]["gpo_open_drain"] = is_open_drain + elif ch423_modes[ch423_id]["gpo_open_drain"] != is_open_drain: + raise cv.Invalid( + "CH423 GPO pins (8-23) must all be configured as push-pull or all as open-drain" + ) + + +CH423_PIN_SCHEMA = pins.gpio_base_schema( + CH423GPIOPin, + cv.int_range(min=0, max=23), + modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN], +).extend( + { + cv.Required(CONF_CH423): cv.use_id(CH423Component), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH423, CH423_PIN_SCHEMA, pin_mode_check) +async def ch423_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_CH423]) + + cg.add(var.set_parent(parent)) + + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/ch423/ch423.cpp b/esphome/components/ch423/ch423.cpp new file mode 100644 index 0000000000..4abbbe7adf --- /dev/null +++ b/esphome/components/ch423/ch423.cpp @@ -0,0 +1,148 @@ +#include "ch423.h" +#include "esphome/core/log.h" +#include "esphome/core/progmem.h" + +namespace esphome::ch423 { + +static constexpr uint8_t CH423_REG_SYS = 0x24; // Set system parameters (0x48 >> 1) +static constexpr uint8_t CH423_SYS_IO_OE = 0x01; // IO output enable +static constexpr uint8_t CH423_SYS_OD_EN = 0x04; // Open drain enable for OC pins +static constexpr uint8_t CH423_REG_IO = 0x30; // Write/read IO7-IO0 (0x60 >> 1) +static constexpr uint8_t CH423_REG_IO_RD = 0x26; // Read IO7-IO0 (0x4D >> 1, rounded down) +static constexpr uint8_t CH423_REG_OCL = 0x22; // Write OC7-OC0 (0x44 >> 1) +static constexpr uint8_t CH423_REG_OCH = 0x23; // Write OC15-OC8 (0x46 >> 1) + +static const char *const TAG = "ch423"; + +void CH423Component::setup() { + // set outputs before mode + this->write_outputs_(); + // Set system parameters and check for errors + bool success = this->write_reg_(CH423_REG_SYS, this->sys_params_); + // Only read inputs if pins are configured for input (IO_OE not set) + if (success && !(this->sys_params_ & CH423_SYS_IO_OE)) { + success = this->read_inputs_(); + } + if (!success) { + ESP_LOGE(TAG, "CH423 not detected"); + this->mark_failed(); + return; + } + + ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), + this->status_has_error()); +} + +void CH423Component::loop() { + // Clear all the previously read flags. + this->pin_read_flags_ = 0x00; +} + +void CH423Component::dump_config() { + ESP_LOGCONFIG(TAG, "CH423:"); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +void CH423Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (pin < 8) { + if (flags & gpio::FLAG_OUTPUT) { + this->sys_params_ |= CH423_SYS_IO_OE; + } + } else if (pin >= 8 && pin < 24) { + if (flags & gpio::FLAG_OPEN_DRAIN) { + this->sys_params_ |= CH423_SYS_OD_EN; + } + } +} + +bool CH423Component::digital_read(uint8_t 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_flags_ |= (1 << pin); + return (this->input_bits_ & (1 << pin)) != 0; +} + +void CH423Component::digital_write(uint8_t pin, bool value) { + if (value) { + this->output_bits_ |= (1 << pin); + } else { + this->output_bits_ &= ~(1 << pin); + } + this->write_outputs_(); +} + +bool CH423Component::read_inputs_() { + if (this->is_failed()) { + return false; + } + // reading inputs requires IO_OE to be 0 + if (this->sys_params_ & CH423_SYS_IO_OE) { + return false; + } + uint8_t result = this->read_reg_(CH423_REG_IO_RD); + this->input_bits_ = result; + this->status_clear_warning(); + return true; +} + +// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address. +bool CH423Component::write_reg_(uint8_t reg, uint8_t value) { + auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0); + if (err != i2c::ERROR_OK) { + char buf[64]; + ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("write failed for register 0x%X, error %d"), reg, err); + this->status_set_warning(buf); + return false; + } + this->status_clear_warning(); + return true; +} + +uint8_t CH423Component::read_reg_(uint8_t reg) { + uint8_t value; + auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1); + if (err != i2c::ERROR_OK) { + char buf[64]; + ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("read failed for register 0x%X, error %d"), reg, err); + this->status_set_warning(buf); + return 0; + } + this->status_clear_warning(); + return value; +} + +bool CH423Component::write_outputs_() { + bool success = true; + // Write IO7-IO0 + success &= this->write_reg_(CH423_REG_IO, static_cast(this->output_bits_)); + // Write OC7-OC0 + success &= this->write_reg_(CH423_REG_OCL, static_cast(this->output_bits_ >> 8)); + // Write OC15-OC8 + success &= this->write_reg_(CH423_REG_OCH, static_cast(this->output_bits_ >> 16)); + return success; +} + +float CH423Component::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 CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI + +void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } + +void CH423GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); } +size_t CH423GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "EXIO%u via CH423", this->pin_); +} +void CH423GPIOPin::set_flags(gpio::Flags flags) { + flags_ = flags; + this->parent_->pin_mode(this->pin_, flags); +} + +} // namespace esphome::ch423 diff --git a/esphome/components/ch423/ch423.h b/esphome/components/ch423/ch423.h new file mode 100644 index 0000000000..7adc7de6a1 --- /dev/null +++ b/esphome/components/ch423/ch423.h @@ -0,0 +1,67 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::ch423 { + +class CH423Component : public Component, public i2c::I2CDevice { + public: + CH423Component() = default; + + /// Check i2c availability and setup masks + void setup() override; + /// Poll for input changes periodically + void loop() override; + /// Helper function to read the value of a pin. + bool digital_read(uint8_t pin); + /// Helper function to write the value of a pin. + void digital_write(uint8_t pin, bool value); + /// Helper function to set the pin mode of a pin. + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + float get_loop_priority() const override; + void dump_config() override; + + protected: + bool write_reg_(uint8_t reg, uint8_t value); + uint8_t read_reg_(uint8_t reg); + bool read_inputs_(); + bool write_outputs_(); + + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint32_t output_bits_{0x00}; + /// Flags to check if read previously during this loop + uint8_t pin_read_flags_{0x00}; + /// Copy of last read values + uint8_t input_bits_{0x00}; + /// System parameters + uint8_t sys_params_{0x00}; +}; + +/// Helper class to expose a CH423 pin as a GPIO pin. +class CH423GPIOPin : public GPIOPin { + public: + void setup() override{}; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + size_t dump_summary(char *buffer, size_t len) const override; + + void set_parent(CH423Component *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags); + + gpio::Flags get_flags() const override { return this->flags_; } + + protected: + CH423Component *parent_{}; + uint8_t pin_{}; + bool inverted_{}; + gpio::Flags flags_{}; +}; + +} // namespace esphome::ch423 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1d7ea5395f..a3154f9933 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -739,9 +739,10 @@ CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram" CONF_DISABLE_FATFS = "disable_fatfs" # VFS requirement tracking -# Components that need VFS features can call require_vfs_select() or require_vfs_dir() +# Components that need VFS features can call require_vfs_*() functions KEY_VFS_SELECT_REQUIRED = "vfs_select_required" KEY_VFS_DIR_REQUIRED = "vfs_dir_required" +KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required" # Feature requirement tracking - components can call require_* functions to re-enable # These are stored in CORE.data[KEY_ESP32] dict KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required" @@ -768,6 +769,15 @@ def require_vfs_dir() -> None: CORE.data[KEY_VFS_DIR_REQUIRED] = True +def require_vfs_termios() -> None: + """Mark that VFS termios support is required by a component. + + Call this from components that use terminal I/O functions (usb_serial_jtag_vfs_*, etc.). + This prevents CONFIG_VFS_SUPPORT_TERMIOS from being disabled. + """ + CORE.data[KEY_VFS_TERMIOS_REQUIRED] = True + + def require_full_certificate_bundle() -> None: """Request the full certificate bundle instead of the common-CAs-only bundle. @@ -1372,11 +1382,18 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) # Disable VFS support for termios (terminal I/O functions) - # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). + # USB Serial JTAG VFS functions require termios support. + # Components that need it (e.g., logger when USB_SERIAL_JTAG is supported but not selected + # as the logger output) call require_vfs_termios(). # Saves approximately 1.8KB of flash when disabled (default). - add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS] - ) + if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False): + # Component requires VFS termios - force enable regardless of user setting + add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_TERMIOS", True) + else: + # No component needs it - allow user to control (default: disabled) + add_idf_sdkconfig_option( + "CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS] + ) # Disable VFS support for select() with file descriptors # ESPHome only uses select() with sockets via lwip_select(), which still works. diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index cadd0a14ae..40ceaec7dc 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -16,6 +16,8 @@ from esphome.components.esp32 import ( VARIANT_ESP32S3, add_idf_sdkconfig_option, get_esp32_variant, + require_usb_serial_jtag_secondary, + require_vfs_termios, ) from esphome.components.libretiny import get_libretiny_component, get_libretiny_family from esphome.components.libretiny.const import ( @@ -397,9 +399,15 @@ async def to_code(config): elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG") + # Define platform support flags for components that need auto-detection try: uart_selection(USB_SERIAL_JTAG) cg.add_define("USE_LOGGER_USB_SERIAL_JTAG") + # USB Serial JTAG code is compiled when platform supports it. + # Enable secondary USB serial JTAG console so the VFS functions are available. + if CORE.is_esp32 and config[CONF_HARDWARE_UART] != USB_SERIAL_JTAG: + require_usb_serial_jtag_secondary() + require_vfs_termios() except cv.Invalid: pass try: diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index bb167033d1..b0920de062 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -2,21 +2,20 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace pmsx003 { +namespace esphome::pmsx003 { static const char *const TAG = "pmsx003"; static const uint8_t START_CHARACTER_1 = 0x42; static const uint8_t START_CHARACTER_2 = 0x4D; -static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on in ms +static const uint16_t STABILISING_MS = 30000; // time taken for the sensor to become stable after power on in ms -static const uint16_t PMS_CMD_MEASUREMENT_MODE_PASSIVE = - 0x0000; // use `PMS_CMD_MANUAL_MEASUREMENT` to trigger a measurement -static const uint16_t PMS_CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automatically perform measurements -static const uint16_t PMS_CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode -static const uint16_t PMS_CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode +static const uint16_t CMD_MEASUREMENT_MODE_PASSIVE = + 0x0000; // use `Command::MANUAL_MEASUREMENT` to trigger a measurement +static const uint16_t CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automatically perform measurements +static const uint16_t CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode +static const uint16_t CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode void PMSX003Component::setup() {} @@ -42,7 +41,7 @@ void PMSX003Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); - if (this->update_interval_ <= PMS_STABILISING_MS) { + if (this->update_interval_ <= STABILISING_MS) { ESP_LOGCONFIG(TAG, " Mode: active continuous (sensor default)"); } else { ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles"); @@ -55,44 +54,44 @@ void PMSX003Component::loop() { const uint32_t now = App.get_loop_component_start_time(); // Initialize sensor mode on first loop - if (this->initialised_ == 0) { - if (this->update_interval_ > PMS_STABILISING_MS) { + if (!this->initialised_) { + if (this->update_interval_ > STABILISING_MS) { // Long update interval: use passive mode with sleep/wake cycles - this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE); - this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP); + this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_PASSIVE); + this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP); } else { // Short/zero update interval: use active continuous mode - this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_ACTIVE); + this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_ACTIVE); } - this->initialised_ = 1; + this->initialised_ = true; } // If we update less often than it takes the device to stabilise, spin the fan down // rather than running it constantly. It does take some time to stabilise, so we // need to keep track of what state we're in. - if (this->update_interval_ > PMS_STABILISING_MS) { + if (this->update_interval_ > STABILISING_MS) { switch (this->state_) { - case PMSX003_STATE_IDLE: + case State::IDLE: // Power on the sensor now so it'll be ready when we hit the update time - if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS)) + if (now - this->last_update_ < (this->update_interval_ - STABILISING_MS)) return; - this->state_ = PMSX003_STATE_STABILISING; - this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP); + this->state_ = State::STABILISING; + this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP); this->fan_on_time_ = now; return; - case PMSX003_STATE_STABILISING: + case State::STABILISING: // wait for the sensor to be stable - if (now - this->fan_on_time_ < PMS_STABILISING_MS) + if (now - this->fan_on_time_ < STABILISING_MS) return; // consume any command responses that are in the serial buffer while (this->available()) this->read_byte(&this->data_[0]); // Trigger a new read - this->send_command_(PMS_CMD_MANUAL_MEASUREMENT, 0); - this->state_ = PMSX003_STATE_WAITING; + this->send_command_(Command::MANUAL_MEASUREMENT, 0); + this->state_ = State::WAITING; break; - case PMSX003_STATE_WAITING: + case State::WAITING: // Just go ahead and read stuff break; } @@ -180,27 +179,28 @@ optional PMSX003Component::check_byte_() { } bool PMSX003Component::check_payload_length_(uint16_t payload_length) { + // https://avaldebe.github.io/PyPMS/sensors/Plantower/ switch (this->type_) { - case PMSX003_TYPE_X003: + case Type::PMSX003: // The expected payload length is typically 28 bytes. // However, a 20-byte payload check was already present in the code. // No official documentation was found confirming this. // Retaining this check to avoid breaking existing behavior. return payload_length == 28 || payload_length == 20; // 2*13+2 - case PMSX003_TYPE_5003T: - case PMSX003_TYPE_5003S: + case Type::PMS5003S: + case Type::PMS5003T: return payload_length == 28; // 2*13+2 (Data 13 not set/reserved) - case PMSX003_TYPE_5003ST: + case Type::PMS5003ST: return payload_length == 36; // 2*17+2 (Data 16 not set/reserved) } return false; } -void PMSX003Component::send_command_(PMSX0003Command cmd, uint16_t data) { +void PMSX003Component::send_command_(Command cmd, uint16_t data) { uint8_t send_data[7] = { START_CHARACTER_1, // Start Byte 1 START_CHARACTER_2, // Start Byte 2 - cmd, // Command + static_cast(cmd), // Command uint8_t((data >> 8) & 0xFF), // Data 1 uint8_t((data >> 0) & 0xFF), // Data 2 0, // Verify Byte 1 @@ -265,7 +265,7 @@ void PMSX003Component::parse_data_() { if (this->pm_particles_25um_sensor_ != nullptr) this->pm_particles_25um_sensor_->publish_state(pm_particles_25um); - if (this->type_ == PMSX003_TYPE_5003T) { + if (this->type_ == Type::PMS5003T) { ESP_LOGD(TAG, "Got PM0.3 Particles: %u Count/0.1L, PM0.5 Particles: %u Count/0.1L, PM1.0 Particles: %u Count/0.1L, " "PM2.5 Particles %u Count/0.1L", @@ -289,7 +289,7 @@ void PMSX003Component::parse_data_() { } // Formaldehyde - if (this->type_ == PMSX003_TYPE_5003ST || this->type_ == PMSX003_TYPE_5003S) { + if (this->type_ == Type::PMS5003S || this->type_ == Type::PMS5003ST) { const uint16_t formaldehyde = this->get_16_bit_uint_(28); ESP_LOGD(TAG, "Got Formaldehyde: %u µg/m^3", formaldehyde); @@ -299,8 +299,8 @@ void PMSX003Component::parse_data_() { } // Temperature and Humidity - if (this->type_ == PMSX003_TYPE_5003ST || this->type_ == PMSX003_TYPE_5003T) { - const uint8_t temperature_offset = (this->type_ == PMSX003_TYPE_5003T) ? 24 : 30; + if (this->type_ == Type::PMS5003T || this->type_ == Type::PMS5003ST) { + const uint8_t temperature_offset = (this->type_ == Type::PMS5003T) ? 24 : 30; const float temperature = static_cast(this->get_16_bit_uint_(temperature_offset)) / 10.0f; const float humidity = this->get_16_bit_uint_(temperature_offset + 2) / 10.0f; @@ -314,7 +314,7 @@ void PMSX003Component::parse_data_() { } // Firmware Version and Error Code - if (this->type_ == PMSX003_TYPE_5003ST) { + if (this->type_ == Type::PMS5003ST) { const uint8_t firmware_version = this->data_[36]; const uint8_t error_code = this->data_[37]; @@ -323,13 +323,12 @@ void PMSX003Component::parse_data_() { // Spin down the sensor again if we aren't going to need it until more time has // passed than it takes to stabilise - if (this->update_interval_ > PMS_STABILISING_MS) { - this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_SLEEP); - this->state_ = PMSX003_STATE_IDLE; + if (this->update_interval_ > STABILISING_MS) { + this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_SLEEP); + this->state_ = State::IDLE; } this->status_clear_warning(); } -} // namespace pmsx003 -} // namespace esphome +} // namespace esphome::pmsx003 diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index f48121800e..f2d4e68db7 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -5,27 +5,25 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace pmsx003 { +namespace esphome::pmsx003 { -enum PMSX0003Command : uint8_t { - PMS_CMD_MEASUREMENT_MODE = - 0xE1, // Data Options: `PMS_CMD_MEASUREMENT_MODE_PASSIVE`, `PMS_CMD_MEASUREMENT_MODE_ACTIVE` - PMS_CMD_MANUAL_MEASUREMENT = 0xE2, - PMS_CMD_SLEEP_MODE = 0xE4, // Data Options: `PMS_CMD_SLEEP_MODE_SLEEP`, `PMS_CMD_SLEEP_MODE_WAKEUP` +enum class Type : uint8_t { + PMSX003 = 0, + PMS5003S, + PMS5003T, + PMS5003ST, }; -enum PMSX003Type { - PMSX003_TYPE_X003 = 0, - PMSX003_TYPE_5003T, - PMSX003_TYPE_5003ST, - PMSX003_TYPE_5003S, +enum class Command : uint8_t { + MEASUREMENT_MODE = 0xE1, // Data Options: `CMD_MEASUREMENT_MODE_PASSIVE`, `CMD_MEASUREMENT_MODE_ACTIVE` + MANUAL_MEASUREMENT = 0xE2, + SLEEP_MODE = 0xE4, // Data Options: `CMD_SLEEP_MODE_SLEEP`, `CMD_SLEEP_MODE_WAKEUP` }; -enum PMSX003State { - PMSX003_STATE_IDLE = 0, - PMSX003_STATE_STABILISING, - PMSX003_STATE_WAITING, +enum class State : uint8_t { + IDLE = 0, + STABILISING, + WAITING, }; class PMSX003Component : public uart::UARTDevice, public Component { @@ -37,7 +35,7 @@ class PMSX003Component : public uart::UARTDevice, public Component { void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } - void set_type(PMSX003Type type) { this->type_ = type; } + void set_type(Type type) { this->type_ = type; } void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor) { this->pm_1_0_std_sensor_ = pm_1_0_std_sensor; } void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor) { this->pm_2_5_std_sensor_ = pm_2_5_std_sensor; } @@ -77,20 +75,20 @@ class PMSX003Component : public uart::UARTDevice, public Component { optional check_byte_(); void parse_data_(); bool check_payload_length_(uint16_t payload_length); - void send_command_(PMSX0003Command cmd, uint16_t data); + void send_command_(Command cmd, uint16_t data); uint16_t get_16_bit_uint_(uint8_t start_index) const { return encode_uint16(this->data_[start_index], this->data_[start_index + 1]); } + Type type_; + State state_{State::IDLE}; + bool initialised_{false}; uint8_t data_[64]; uint8_t data_index_{0}; - uint8_t initialised_{0}; uint32_t fan_on_time_{0}; uint32_t last_update_{0}; uint32_t last_transmission_{0}; uint32_t update_interval_{0}; - PMSX003State state_{PMSX003_STATE_IDLE}; - PMSX003Type type_; // "Standard Particle" sensor::Sensor *pm_1_0_std_sensor_{nullptr}; @@ -118,5 +116,4 @@ class PMSX003Component : public uart::UARTDevice, public Component { sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace pmsx003 -} // namespace esphome +} // namespace esphome::pmsx003 diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index bebd3a01ee..4f95b1dcdb 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -41,33 +41,33 @@ PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Com PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor) TYPE_PMSX003 = "PMSX003" +TYPE_PMS5003S = "PMS5003S" TYPE_PMS5003T = "PMS5003T" TYPE_PMS5003ST = "PMS5003ST" -TYPE_PMS5003S = "PMS5003S" -PMSX003Type = pmsx003_ns.enum("PMSX003Type") +Type = pmsx003_ns.enum("Type", is_class=True) PMSX003_TYPES = { - TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003, - TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, - TYPE_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST, - TYPE_PMS5003S: PMSX003Type.PMSX003_TYPE_5003S, + TYPE_PMSX003: Type.PMSX003, + TYPE_PMS5003S: Type.PMS5003S, + TYPE_PMS5003T: Type.PMS5003T, + TYPE_PMS5003ST: Type.PMS5003ST, } SENSORS_TO_TYPE = { - CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_1_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_2_5_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_10_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_0_3UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_0_5UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_1_0UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_2_5UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_5_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_10_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_FORMALDEHYDE: [TYPE_PMS5003ST, TYPE_PMS5003S], + CONF_PM_1_0_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_2_5_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_10_0_STD: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_0_3UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_0_5UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_1_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_2_5UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_5_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003ST], + CONF_PM_10_0UM: [TYPE_PMSX003, TYPE_PMS5003S, TYPE_PMS5003ST], + CONF_FORMALDEHYDE: [TYPE_PMS5003S, TYPE_PMS5003ST], CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST], CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST], } diff --git a/tests/components/ch423/common.yaml b/tests/components/ch423/common.yaml new file mode 100644 index 0000000000..ccf9170bd0 --- /dev/null +++ b/tests/components/ch423/common.yaml @@ -0,0 +1,36 @@ +ch423: + - id: ch423_hub + i2c_id: i2c_bus + +binary_sensor: + - platform: gpio + id: ch423_input + name: CH423 Binary Sensor + pin: + ch423: ch423_hub + number: 1 + mode: INPUT + inverted: true + - platform: gpio + id: ch423_input_2 + name: CH423 Binary Sensor 2 + pin: + ch423: ch423_hub + number: 0 + mode: INPUT + inverted: false +output: + - platform: gpio + id: ch423_out_11 + pin: + ch423: ch423_hub + number: 11 + mode: OUTPUT_OPEN_DRAIN + inverted: true + - platform: gpio + id: ch423_out_23 + pin: + ch423: ch423_hub + number: 23 + mode: OUTPUT_OPEN_DRAIN + inverted: false diff --git a/tests/components/ch423/test.esp32-idf.yaml b/tests/components/ch423/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/ch423/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/ch423/test.esp8266-ard.yaml b/tests/components/ch423/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/ch423/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/ch423/test.rp2040-ard.yaml b/tests/components/ch423/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/ch423/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/pmsx003/common.yaml b/tests/components/pmsx003/common.yaml index 3c60995804..eaa3cbc3e9 100644 --- a/tests/components/pmsx003/common.yaml +++ b/tests/components/pmsx003/common.yaml @@ -8,11 +8,11 @@ sensor: pm_10_0: name: PM 10.0 Concentration pm_1_0_std: - name: PM 1.0 Standard Atmospher Concentration + name: PM 1.0 Standard Atmospheric Concentration pm_2_5_std: - name: PM 2.5 Standard Atmospher Concentration + name: PM 2.5 Standard Atmospheric Concentration pm_10_0_std: - name: PM 10.0 Standard Atmospher Concentration + name: PM 10.0 Standard Atmospheric Concentration pm_0_3um: name: Particulate Count >0.3um pm_0_5um: diff --git a/tests/unit_tests/components/test_ch423.py b/tests/unit_tests/components/test_ch423.py new file mode 100644 index 0000000000..ac79fe48fe --- /dev/null +++ b/tests/unit_tests/components/test_ch423.py @@ -0,0 +1,58 @@ +"""Tests for ch423 component validation.""" + +from unittest.mock import patch + +from esphome import config, yaml_util +from esphome.core import CORE + + +def test_ch423_mixed_gpio_modes_fails(tmp_path, capsys): + """Test that mixing input/output on GPIO pins 0-7 fails validation.""" + test_file = tmp_path / "test.yaml" + test_file.write_text(""" +esphome: + name: test + +esp8266: + board: esp01_1m + +i2c: + sda: GPIO4 + scl: GPIO5 + +ch423: + - id: ch423_hub + +binary_sensor: + - platform: gpio + name: "CH423 Input 0" + pin: + ch423: ch423_hub + number: 0 + mode: input + +switch: + - platform: gpio + name: "CH423 Output 1" + pin: + ch423: ch423_hub + number: 1 + mode: output +""") + + parsed_yaml = yaml_util.load_yaml(test_file) + + with ( + patch.object(yaml_util, "load_yaml", return_value=parsed_yaml), + patch.object(CORE, "config_path", test_file), + ): + result = config.read_config({}) + + assert result is None, "Expected validation to fail with mixed GPIO modes" + + # Check that the error message mentions the GPIO pin restriction + captured = capsys.readouterr() + assert ( + "GPIO pins (0-7) must all be configured as input or all as output" + in captured.out + )