From 4c1363b104f198aa837a11bd1ba1ecad28371ec4 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:07:40 -0400 Subject: [PATCH 1/5] [spi] Add LOG_SPI_DEVICE macro (#15118) --- esphome/components/spi/spi.h | 2 ++ script/ci-custom.py | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index e237cf44f4..84c8bca267 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -34,6 +34,8 @@ using SPIInterface = void *; // Stub for platforms without SPI (e.g., Zephyr) */ namespace esphome::spi { +#define LOG_SPI_DEVICE(this) ESP_LOGCONFIG(TAG, " CS Pin: %d", esphome::spi::Utility::get_pin_no(this->cs_)); + /// The bit-order for SPI devices. This defines how the data read from and written to the device is interpreted. enum SPIBitOrder { /// The least significant bit is transmitted/received first. diff --git a/script/ci-custom.py b/script/ci-custom.py index 1da923b095..25a0cf2127 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -963,6 +963,7 @@ def lint_log_multiline_continuation(fname, content): "esphome/components/nextion/nextion_base.h", "esphome/components/select/select.h", "esphome/components/sensor/sensor.h", + "esphome/components/spi/spi.h", "esphome/components/stepper/stepper.h", "esphome/components/switch/switch.h", "esphome/components/text/text.h", From 1e16b30380a4703142318a5a1a93f6ad3626f6f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2026 09:18:58 -1000 Subject: [PATCH 2/5] [ethernet] Add ENC28J60 SPI Ethernet support (#14945) --- esphome/components/ethernet/__init__.py | 44 ++++++++++++++----- .../components/ethernet/ethernet_component.h | 13 ++++++ .../ethernet/ethernet_component_esp32.cpp | 41 +++++++++++------ .../ethernet/ethernet_component_rp2040.cpp | 27 +++++++++--- .../ethernet/common-enc28j60-rp2040.yaml | 18 ++++++++ .../components/ethernet/common-enc28j60.yaml | 19 ++++++++ .../ethernet/test-enc28j60.esp32-idf.yaml | 1 + .../ethernet/test-enc28j60.rp2040-ard.yaml | 1 + 8 files changed, 134 insertions(+), 30 deletions(-) create mode 100644 tests/components/ethernet/common-enc28j60-rp2040.yaml create mode 100644 tests/components/ethernet/common-enc28j60.yaml create mode 100644 tests/components/ethernet/test-enc28j60.esp32-idf.yaml create mode 100644 tests/components/ethernet/test-enc28j60.rp2040-ard.yaml diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index f519d79aa1..e17abfcc93 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -119,6 +119,7 @@ ETHERNET_TYPES = { "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, "DM9051": EthernetType.ETHERNET_TYPE_DM9051, "LAN8670": EthernetType.ETHERNET_TYPE_LAN8670, + "ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60, } # PHY types that need compile-time defines for conditional compilation @@ -134,6 +135,7 @@ _PHY_TYPE_TO_DEFINE = { "W5500": "USE_ETHERNET_W5500", "DM9051": "USE_ETHERNET_DM9051", "LAN8670": "USE_ETHERNET_LAN8670", + "ENC28J60": "USE_ETHERNET_ENC28J60", } @@ -155,11 +157,16 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = { "KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"), "W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"), "DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"), + "ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"), + "LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"), } -SPI_ETHERNET_TYPES = ["W5500", "DM9051"] +# These types are always external IDF components (never built-in to ESP-IDF) +_ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"} + +SPI_ETHERNET_TYPES = ["W5500", "DM9051", "ENC28J60"] # RP2040-supported SPI ethernet types -RP2040_SPI_ETHERNET_TYPES = ["W5500"] +RP2040_SPI_ETHERNET_TYPES = ["W5500", "ENC28J60"] SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") @@ -220,7 +227,18 @@ def _validate(config): if CORE.is_esp32: if config[CONF_TYPE] in SPI_ETHERNET_TYPES: - if _is_framework_spi_polling_mode_supported(): + # ENC28J60 driver does not support polling mode - interrupt is required + if config[CONF_TYPE] == "ENC28J60": + if CONF_POLLING_INTERVAL in config: + raise cv.Invalid( + f"'{CONF_POLLING_INTERVAL}' is not supported for ENC28J60. " + f"'{CONF_INTERRUPT_PIN}' is required." + ) + if CONF_INTERRUPT_PIN not in config: + raise cv.Invalid( + f"'{CONF_INTERRUPT_PIN}' is a required option for ENC28J60." + ) + elif _is_framework_spi_polling_mode_supported(): if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config: raise cv.Invalid( f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}" @@ -367,6 +385,7 @@ CONFIG_SCHEMA = cv.All( "W5500": SPI_SCHEMA, "OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])), "DM9051": SPI_SCHEMA, + "ENC28J60": SPI_SCHEMA, "LAN8670": RMII_SCHEMA, }, upper=True, @@ -502,7 +521,8 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: cg.add_define("USE_ETHERNET_SPI") add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) # CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0 - if idf_version() < cv.Version(6, 0, 0): + # ENC28J60 was never built-in to IDF, so it has no Kconfig option + if idf_version() < cv.Version(6, 0, 0) and config[CONF_TYPE] != "ENC28J60": add_idf_sdkconfig_option( f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True ) @@ -533,12 +553,11 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: # Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time) include_builtin_idf_component("esp_eth") - if config[CONF_TYPE] == "LAN8670": - # Add LAN867x 10BASE-T1S PHY support component - add_idf_component(name="espressif/lan867x", ref="2.0.0") - - # IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry - if idf_version() >= cv.Version(6, 0, 0) and ( + if config[CONF_TYPE] in _ALWAYS_EXTERNAL_IDF_COMPONENTS: + component = _IDF6_ETHERNET_COMPONENTS[config[CONF_TYPE]] + add_idf_component(name=component.name, ref=component.version) + elif idf_version() >= cv.Version(6, 0, 0) and ( + # IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry component := _IDF6_ETHERNET_COMPONENTS.get(config[CONF_TYPE]) ): add_idf_component(name=component.name, ref=component.version) @@ -555,7 +574,10 @@ async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None: cg.add(var.set_reset_pin(config[CONF_RESET_PIN])) cg.add_define("USE_ETHERNET_SPI") - cg.add_library("lwIP_w5500", None) + if config[CONF_TYPE] == "ENC28J60": + cg.add_library("lwIP_enc28j60", None) + else: + cg.add_library("lwIP_w5500", None) def _final_validate_rmii_pins(config: ConfigType) -> None: diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index b6699e8020..4c85c39eb8 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -23,7 +23,13 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); #endif // USE_ESP32 #ifdef USE_RP2040 +#if defined(USE_ETHERNET_W5500) #include +#elif defined(USE_ETHERNET_ENC28J60) +#include +#else +#error "Unsupported RP2040 SPI Ethernet type" +#endif #endif namespace esphome::ethernet { @@ -57,6 +63,7 @@ enum EthernetType : uint8_t { ETHERNET_TYPE_OPENETH, ETHERNET_TYPE_DM9051, ETHERNET_TYPE_LAN8670, + ETHERNET_TYPE_ENC28J60, }; struct ManualIP { @@ -215,7 +222,13 @@ class EthernetComponent final : public Component { #ifdef USE_RP2040 static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls +#if defined(USE_ETHERNET_W5500) Wiznet5500lwIP *eth_{nullptr}; +#elif defined(USE_ETHERNET_ENC28J60) + ENC28J60lwIP *eth_{nullptr}; +#else +#error "Unsupported RP2040 SPI Ethernet type" +#endif uint32_t last_link_check_{0}; uint8_t clk_pin_; uint8_t miso_pin_; diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp index fb69a901aa..a170239e03 100644 --- a/esphome/components/ethernet/ethernet_component_esp32.cpp +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -44,6 +44,11 @@ #include "esp_eth_phy_lan867x.h" #endif +// ENC28J60 header exists on all IDF versions (always an external component) +#ifdef USE_ETHERNET_ENC28J60 +#include "esp_eth_enc28j60.h" +#endif + #ifdef USE_ETHERNET_SPI #include #include @@ -194,25 +199,27 @@ void EthernetComponent::setup() { .post_cb = nullptr, }; -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); -#endif -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); +#elif defined(USE_ETHERNET_ENC28J60) + eth_enc28j60_config_t enc28j60_config = ETH_ENC28J60_DEFAULT_CONFIG(host, &devcfg); #endif -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) w5500_config.int_gpio_num = this->interrupt_pin_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT w5500_config.poll_period_ms = this->polling_interval_; #endif -#endif - -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) dm9051_config.int_gpio_num = this->interrupt_pin_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT dm9051_config.poll_period_ms = this->polling_interval_; #endif +#elif defined(USE_ETHERNET_ENC28J60) + enc28j60_config.int_gpio_num = this->interrupt_pin_; + // ENC28J60 does not support poll_period_ms #endif phy_config.phy_addr = this->phy_addr_spi_; @@ -300,19 +307,24 @@ void EthernetComponent::setup() { #endif #endif #ifdef USE_ETHERNET_SPI -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) case ETHERNET_TYPE_W5500: { mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); this->phy_ = esp_eth_phy_new_w5500(&phy_config); break; } -#endif -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) case ETHERNET_TYPE_DM9051: { mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); this->phy_ = esp_eth_phy_new_dm9051(&phy_config); break; } +#elif defined(USE_ETHERNET_ENC28J60) + case ETHERNET_TYPE_ENC28J60: { + mac = esp_eth_mac_new_enc28j60(&enc28j60_config, &mac_config); + this->phy_ = esp_eth_phy_new_enc28j60(&phy_config); + break; + } #endif #endif default: { @@ -405,15 +417,18 @@ void EthernetComponent::dump_config() { eth_type = "KSZ8081RNA"; break; #endif -#ifdef USE_ETHERNET_W5500 +#if defined(USE_ETHERNET_W5500) case ETHERNET_TYPE_W5500: eth_type = "W5500"; break; -#endif -#ifdef USE_ETHERNET_DM9051 +#elif defined(USE_ETHERNET_DM9051) case ETHERNET_TYPE_DM9051: eth_type = "DM9051"; break; +#elif defined(USE_ETHERNET_ENC28J60) + case ETHERNET_TYPE_ENC28J60: + eth_type = "ENC28J60"; + break; #endif #ifdef USE_ETHERNET_OPENETH case ETHERNET_TYPE_OPENETH: diff --git a/esphome/components/ethernet/ethernet_component_rp2040.cpp b/esphome/components/ethernet/ethernet_component_rp2040.cpp index 77b1a22d66..bd8c458985 100644 --- a/esphome/components/ethernet/ethernet_component_rp2040.cpp +++ b/esphome/components/ethernet/ethernet_component_rp2040.cpp @@ -31,11 +31,15 @@ void EthernetComponent::setup() { reset_pin.digital_write(false); delay(1); // NOLINT reset_pin.digital_write(true); - delay(10); // NOLINT - wait for W5500 to initialize after reset + delay(10); // NOLINT - wait for chip to initialize after reset } - // Create the W5500 device instance + // Create the SPI Ethernet device instance +#if defined(USE_ETHERNET_W5500) this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#elif defined(USE_ETHERNET_ENC28J60) + this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#endif // Set hostname before begin() so the LWIP netif gets it this->eth_->hostname(App.get_name().c_str()); @@ -61,7 +65,7 @@ void EthernetComponent::setup() { } if (!success) { - ESP_LOGE(TAG, "Failed to initialize W5500 Ethernet"); + ESP_LOGE(TAG, "Failed to initialize Ethernet"); delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory) this->eth_ = nullptr; this->mark_failed(); @@ -164,9 +168,15 @@ void EthernetComponent::loop() { } void EthernetComponent::dump_config() { + const char *type_str = "Unknown"; +#if defined(USE_ETHERNET_W5500) + type_str = "W5500"; +#elif defined(USE_ETHERNET_ENC28J60) + type_str = "ENC28J60"; +#endif ESP_LOGCONFIG(TAG, "Ethernet:\n" - " Type: W5500\n" + " Type: %s\n" " Connected: %s\n" " CLK Pin: %u\n" " MISO Pin: %u\n" @@ -174,7 +184,7 @@ void EthernetComponent::dump_config() { " CS Pin: %u\n" " IRQ Pin: %d\n" " Reset Pin: %d", - YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_, + type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_, this->interrupt_pin_, this->reset_pin_); this->dump_connect_params_(); } @@ -216,13 +226,18 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( } eth_duplex_t EthernetComponent::get_duplex_mode() { - // W5500 is always full duplex + // Both W5500 and ENC28J60 are full-duplex on RP2040 return ETH_DUPLEX_FULL; } eth_speed_t EthernetComponent::get_link_speed() { +#ifdef USE_ETHERNET_ENC28J60 + // ENC28J60 is 10Mbps only + return ETH_SPEED_10M; +#else // W5500 is always 100Mbps return ETH_SPEED_100M; +#endif } bool EthernetComponent::powerdown() { diff --git a/tests/components/ethernet/common-enc28j60-rp2040.yaml b/tests/components/ethernet/common-enc28j60-rp2040.yaml new file mode 100644 index 0000000000..0718723d8a --- /dev/null +++ b/tests/components/ethernet/common-enc28j60-rp2040.yaml @@ -0,0 +1,18 @@ +ethernet: + type: ENC28J60 + clk_pin: 18 + mosi_pin: 19 + miso_pin: 16 + cs_pin: 17 + interrupt_pin: 21 + reset_pin: 20 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-enc28j60.yaml b/tests/components/ethernet/common-enc28j60.yaml new file mode 100644 index 0000000000..6b288bed19 --- /dev/null +++ b/tests/components/ethernet/common-enc28j60.yaml @@ -0,0 +1,19 @@ +ethernet: + type: ENC28J60 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/test-enc28j60.esp32-idf.yaml b/tests/components/ethernet/test-enc28j60.esp32-idf.yaml new file mode 100644 index 0000000000..b7c1b7f5aa --- /dev/null +++ b/tests/components/ethernet/test-enc28j60.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-enc28j60.yaml diff --git a/tests/components/ethernet/test-enc28j60.rp2040-ard.yaml b/tests/components/ethernet/test-enc28j60.rp2040-ard.yaml new file mode 100644 index 0000000000..61d1cd86f5 --- /dev/null +++ b/tests/components/ethernet/test-enc28j60.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enc28j60-rp2040.yaml From 11b829dda17b8cca2174272069caf36d0dca2628 Mon Sep 17 00:00:00 2001 From: Daniel Kent <129895318+danielkent-net@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:59:17 -0400 Subject: [PATCH 3/5] [spa06_spi] Add SPA06-003 Temperature and Pressure Sensor - SPI support (Part 3 of 3) (#14523) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/spa06_spi/__init__.py | 0 esphome/components/spa06_spi/sensor.py | 41 +++++++++++ esphome/components/spa06_spi/spa06_spi.cpp | 72 +++++++++++++++++++ esphome/components/spa06_spi/spa06_spi.h | 22 ++++++ tests/components/spa06_spi/common.yaml | 15 ++++ .../components/spa06_spi/test.esp32-idf.yaml | 7 ++ .../spa06_spi/test.esp8266-ard.yaml | 7 ++ .../components/spa06_spi/test.rp2040-ard.yaml | 7 ++ 9 files changed, 172 insertions(+) create mode 100644 esphome/components/spa06_spi/__init__.py create mode 100644 esphome/components/spa06_spi/sensor.py create mode 100644 esphome/components/spa06_spi/spa06_spi.cpp create mode 100644 esphome/components/spa06_spi/spa06_spi.h create mode 100644 tests/components/spa06_spi/common.yaml create mode 100644 tests/components/spa06_spi/test.esp32-idf.yaml create mode 100644 tests/components/spa06_spi/test.esp8266-ard.yaml create mode 100644 tests/components/spa06_spi/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index e3e09cbc11..afe4cdb871 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -459,6 +459,7 @@ esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt esphome/components/spa06_base/* @danielkent-net esphome/components/spa06_i2c/* @danielkent-net +esphome/components/spa06_spi/* @danielkent-net esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker_source/* @kahrendt diff --git a/esphome/components/spa06_spi/__init__.py b/esphome/components/spa06_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/spa06_spi/sensor.py b/esphome/components/spa06_spi/sensor.py new file mode 100644 index 0000000000..b82186ec21 --- /dev/null +++ b/esphome/components/spa06_spi/sensor.py @@ -0,0 +1,41 @@ +import logging + +import esphome.codegen as cg +from esphome.components import spi +from esphome.components.spi import CONF_SPI_MODE +import esphome.config_validation as cv + +from ..spa06_base import CONFIG_SCHEMA_BASE, to_code_base + +AUTO_LOAD = ["spa06_base"] +CODEOWNERS = ["@danielkent-net"] +DEPENDENCIES = ["spi"] + +spa06_ns = cg.esphome_ns.namespace("spa06_spi") +SPA06SPIComponent = spa06_ns.class_( + "SPA06SPIComponent", cg.PollingComponent, spi.SPIDevice +) + +_LOGGER = logging.getLogger(__name__) + +VALID_SPI_MODES = {3: "MODE3", "3": "MODE3", "MODE3": "MODE3"} + + +def check_spi_mode(config): + spi_mode = config.get(CONF_SPI_MODE) + if spi_mode not in VALID_SPI_MODES: + raise cv.Invalid("SPA06 only supports SPI mode 3") + return config + + +CONFIG_SCHEMA = cv.All( + CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema(default_mode="mode3")).extend( + {cv.GenerateID(): cv.declare_id(SPA06SPIComponent)} + ), + check_spi_mode, +) + + +async def to_code(config): + var = await to_code_base(config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/spa06_spi/spa06_spi.cpp b/esphome/components/spa06_spi/spa06_spi.cpp new file mode 100644 index 0000000000..9f3683d219 --- /dev/null +++ b/esphome/components/spa06_spi/spa06_spi.cpp @@ -0,0 +1,72 @@ +#include +#include + +#include "spa06_spi.h" +#include "esphome/components/spa06_base/spa06_base.h" +#include "esphome/components/spi/spi.h" + +// OR (|) register with SPA06_SPI_READ for read. +inline constexpr uint8_t SPA06_SPI_READ = 0x80; + +// AND (&) register with SPA06_SPI_WRITE for write. +inline constexpr uint8_t SPA06_SPI_WRITE = 0x7F; + +namespace esphome::spa06_spi { + +static const char *const TAG = "spa06_spi"; + +void SPA06SPIComponent::dump_config() { + SPA06Component::dump_config(); + LOG_SPI_DEVICE(this) +} + +void SPA06SPIComponent::setup() { + this->spi_setup(); + SPA06Component::setup(); +} + +void SPA06SPIComponent::protocol_reset() { + // Forces the device into SPI mode using a dummy read + uint8_t dummy_read = 0; + this->spa_read_byte(spa06_base::SPA06_ID, &dummy_read); +} + +// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address +// is not used and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read). +// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, +// the byte 0x77 is transferred, for read access, the byte 0xF7 is transferred. +// The expressions SPA06_SPI_READ (| with register) and SPA06_SPI_WRITE (& with register) +// are defined for readability. + +bool SPA06SPIComponent::spa_read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + this->transfer_byte(a_register | SPA06_SPI_READ); + *data = this->transfer_byte(0); + this->disable(); + return true; +} + +bool SPA06SPIComponent::spa_write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->transfer_byte(a_register & SPA06_SPI_WRITE); + this->transfer_byte(data); + this->disable(); + return true; +} + +bool SPA06SPIComponent::spa_read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register | SPA06_SPI_READ); + this->read_array(data, len); + this->disable(); + return true; +} + +bool SPA06SPIComponent::spa_write_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register & SPA06_SPI_WRITE); + this->write_array(data, len); + this->disable(); + return true; +} +} // namespace esphome::spa06_spi diff --git a/esphome/components/spa06_spi/spa06_spi.h b/esphome/components/spa06_spi/spa06_spi.h new file mode 100644 index 0000000000..ffbc162d6f --- /dev/null +++ b/esphome/components/spa06_spi/spa06_spi.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/components/spa06_base/spa06_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome::spa06_spi { + +class SPA06SPIComponent : public spa06_base::SPA06Component, + public spi::SPIDevice { + void setup() override; + bool spa_read_byte(uint8_t a_register, uint8_t *data) override; + bool spa_write_byte(uint8_t a_register, uint8_t data) override; + bool spa_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool spa_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + void dump_config() override; + + protected: + void protocol_reset() override; +}; + +} // namespace esphome::spa06_spi diff --git a/tests/components/spa06_spi/common.yaml b/tests/components/spa06_spi/common.yaml new file mode 100644 index 0000000000..9263202ab4 --- /dev/null +++ b/tests/components/spa06_spi/common.yaml @@ -0,0 +1,15 @@ +sensor: + - platform: spa06_spi + spi_id: spi_bus + cs_pin: ${cs_pin} + temperature: + id: spa06_spi_temperature + name: Outside Temperature + sample_rate: 1 + oversampling: NONE + pressure: + name: Outside Pressure + id: spa06_spi_pressure + sample_rate: 25p4 + oversampling: 16X + update_interval: 15s diff --git a/tests/components/spa06_spi/test.esp32-idf.yaml b/tests/components/spa06_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..a3352cf880 --- /dev/null +++ b/tests/components/spa06_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO5 + +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_spi/test.esp8266-ard.yaml b/tests/components/spa06_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..595f31046a --- /dev/null +++ b/tests/components/spa06_spi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO15 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/spa06_spi/test.rp2040-ard.yaml b/tests/components/spa06_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..93e19cfea4 --- /dev/null +++ b/tests/components/spa06_spi/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + cs_pin: GPIO17 + +packages: + spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml + +<<: !include common.yaml From 6956bf7e539589ee06282d8560b97f91989397c3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 24 Mar 2026 07:24:25 +1000 Subject: [PATCH 4/5] [text] Add text_sensor for read-only view of text component (#15090) --- .../components/text/text_sensor/__init__.py | 25 +++++++++++++++++++ .../text/text_sensor/text_text_sensor.cpp | 16 ++++++++++++ .../text/text_sensor/text_text_sensor.h | 19 ++++++++++++++ tests/components/text/common.yaml | 5 ++++ 4 files changed, 65 insertions(+) create mode 100644 esphome/components/text/text_sensor/__init__.py create mode 100644 esphome/components/text/text_sensor/text_text_sensor.cpp create mode 100644 esphome/components/text/text_sensor/text_text_sensor.h diff --git a/esphome/components/text/text_sensor/__init__.py b/esphome/components/text/text_sensor/__init__.py new file mode 100644 index 0000000000..5e45f10193 --- /dev/null +++ b/esphome/components/text/text_sensor/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_SOURCE_ID + +from .. import Text, text_ns + +TextTextSensor = text_ns.class_("TextTextSensor", text_sensor.TextSensor, cg.Component) + + +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema(TextTextSensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(Text), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + source = await cg.get_variable(config[CONF_SOURCE_ID]) + var = await text_sensor.new_text_sensor(config, source) + await cg.register_component(var, config) diff --git a/esphome/components/text/text_sensor/text_text_sensor.cpp b/esphome/components/text/text_sensor/text_text_sensor.cpp new file mode 100644 index 0000000000..50504c605e --- /dev/null +++ b/esphome/components/text/text_sensor/text_text_sensor.cpp @@ -0,0 +1,16 @@ +#include "text_text_sensor.h" +#include "esphome/core/log.h" + +namespace esphome::text { + +static const char *const TAG = "text.text_sensor"; + +void TextTextSensor::setup() { + this->source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); }); + if (this->source_->has_state()) + this->publish_state(this->source_->state); +} + +void TextTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Text Text Sensor", this); } + +} // namespace esphome::text diff --git a/esphome/components/text/text_sensor/text_text_sensor.h b/esphome/components/text/text_sensor/text_text_sensor.h new file mode 100644 index 0000000000..fd70ea3451 --- /dev/null +++ b/esphome/components/text/text_sensor/text_text_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../text.h" +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome::text { + +class TextTextSensor : public text_sensor::TextSensor, public Component { + public: + explicit TextTextSensor(Text *source) : source_(source) {} + void setup() override; + void dump_config() override; + + protected: + Text *source_; +}; + +} // namespace esphome::text diff --git a/tests/components/text/common.yaml b/tests/components/text/common.yaml index 26618be03a..561d17143f 100644 --- a/tests/components/text/common.yaml +++ b/tests/components/text/common.yaml @@ -23,3 +23,8 @@ text: min_length: 8 max_length: 32 mode: password + +text_sensor: + - platform: text + name: "Test Text State" + source_id: test_text From 332118db5644b161c09c90f64e07f2289d6f86a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:44:26 -1000 Subject: [PATCH 5/5] Bump pytest-cov from 7.0.0 to 7.1.0 (#15123) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 0d3f0671f5..1440b20333 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ pre-commit # Unit tests pytest==9.0.2 -pytest-cov==7.0.0 +pytest-cov==7.1.0 pytest-mock==3.15.1 pytest-asyncio==1.3.0 pytest-xdist==3.8.0