Compare commits

..

32 Commits

Author SHA1 Message Date
Jesse Hills 58780cd765 Merge branch 'release' into dev 2026-07-01 16:26:24 +12:00
Jesse Hills 1fc73731a8 Merge pull request #17317 from esphome/bump-2026.6.4
2026.6.4
2026-07-01 16:25:39 +12:00
Jesse Hills e47feace11 Bump version to 2026.6.4 2026-07-01 13:18:54 +12:00
esphome[bot] 4472d3b61b Bump bundled esphome-device-builder to 1.0.23 (#17316) 2026-07-01 13:18:54 +12:00
Jonathan Swoboda 06c5bcbc66 [espnow] Drop oversized received frames to prevent buffer overflow (#17271) 2026-07-01 13:18:54 +12:00
Jonathan Swoboda 6c44775bf5 [bluetooth_proxy] Fix -Wtype-limits warning with active: false (#17273) 2026-07-01 13:18:54 +12:00
Clyde Stubbs b127363fa0 [mipi_spi] Bug fixes (#17247) 2026-07-01 13:18:54 +12:00
esphome[bot] 782b58bbeb Bump bundled esphome-device-builder to 1.0.22 2026-07-01 13:18:50 +12:00
Franck Nijhof 5de508ad8c [es8388] Fix DAC unable to unmute once muted (#17221)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-07-01 13:18:40 +12:00
Jesse Hills 054c8ba485 [config_validation] Fix multicast typo in error message (#17206) 2026-07-01 13:18:35 +12:00
Julian Lunz 3b2be021b2 [adc] Only call cyw43_thread_enter/exit for VSYS when WiFi is active on RP2040 (#17203) 2026-07-01 13:11:21 +12:00
esphome[bot] 848defedd8 Bump bundled esphome-device-builder to 1.0.23 (#17316) 2026-07-01 13:09:48 +12:00
dependabot[bot] 4c9ed129cf Bump awalsh128/cache-apt-pkgs-action from 1.6.0 to 1.6.3 (#17310)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 18:28:04 -04:00
dependabot[bot] c8b37fb1c8 Bump platformdirs from 4.9.4 to 4.10.0 (#17309)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-30 17:18:33 -04:00
Jonathan Swoboda 1b556f5d0c [ethernet] Fix ETH_SPEED_1000M build on IDF 6.0 (enum added in 6.1) (#17311) 2026-06-30 17:17:47 -04:00
Jonathan Swoboda 8a3d0aeafb [tests] Add esp32-c61-idf base file for grouped component tests (#17293) 2026-06-30 19:33:17 +00:00
Jonathan Swoboda 9468ad628c [espnow] Drop oversized received frames to prevent buffer overflow (#17271) 2026-06-30 14:57:34 -04:00
Jonathan Swoboda 990431aa5b [bluetooth_proxy] Fix -Wtype-limits warning with active: false (#17273) 2026-06-30 14:56:30 -04:00
Jonathan Swoboda b79cbcbde7 [espidf] Install native ESP-IDF into a machine-global cache dir (#17306) 2026-06-30 14:56:18 -04:00
Bonne Eggleston afb5922f37 [modbus] Update client components to use ModbusClientDevice (#11987) 2026-06-30 13:26:51 -05:00
Jonathan Swoboda 3035355c0a [ci] Widen import-time margin for CI runner variance (#17287) 2026-06-30 11:38:31 -04:00
Jonathan Swoboda 43b3aa0712 [ci] Fix nRF52 zigbee/network test-grouping conflict (#17295) 2026-06-30 11:37:59 -04:00
Jonathan Swoboda 12b78e7c47 [qmi8658] Pin i2c_id in test config to fix grouped component test conflict (#17303) 2026-06-30 11:37:17 -04:00
Clyde Stubbs fb5d8b5d4c [mipi_spi] Bug fixes (#17247) 2026-06-30 08:43:01 -04:00
Bonne Eggleston 9e72027b64 [devcontainer] Align base image with production, fix Python venv and build tools (#17296)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 08:33:33 -04:00
Edvard Filistovič faa5f72500 [mqtt] Add LN882X (LN882H) platform support (#17297)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-06-30 08:16:18 -04:00
Jonathan Swoboda 359c6a7265 [libretiny] Update LibreTiny to v1.13.0 (#17288) 2026-06-29 22:34:32 -04:00
J. Nick Koston a031da351d Merge pull request #17294 from esphome/bump-device-builder-1.0.22 2026-06-29 19:29:08 -07:00
esphome[bot] 091b6a0ba0 Bump bundled esphome-device-builder to 1.0.22 2026-06-30 02:07:06 +00:00
Jesse Hills cf9d97d5ae [pixoo] Add Divoom Pixoo display component (#16974) 2026-06-30 11:20:48 +12:00
Clyde Stubbs 3e1a6b4e11 [cst9220] Add CST9220 and CST9217 touchscreen support (#16888) 2026-06-30 11:18:01 +12:00
Clyde Stubbs 5c7245dfcd [qmi8658] Motion platform for QMI8658 IMU (#16889) 2026-06-30 11:04:25 +12:00
82 changed files with 4521 additions and 2616 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
ARG BUILD_BASE_VERSION=2025.04.0
ARG BUILD_BASE_VERSION=2026.06.1
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
+5 -2
View File
@@ -15,7 +15,6 @@
// uncomment and edit the path in order to pass through local USB serial to the container
// , "--device=/dev/ttyACM0"
],
"appPort": 6052,
// if you are using avahi in the host device, uncomment these to allow the
// devcontainer to find devices via mdns
//"mounts": [
@@ -41,7 +40,11 @@
],
"settings": {
"python.languageServer": "Pylance",
"python.pythonPath": "/usr/bin/python3",
// Use the container's pre-provisioned venv (built by the Dockerfile, outside the
// bind-mounted workspace) rather than a ./venv that may leak in from the host and
// mismatch the container's Python. See .devcontainer/Dockerfile (esphome-venv).
"python.defaultInterpreterPath": "/home/esphome/.local/esphome-venv/bin/python",
"python.terminal.activateEnvironment": true,
"pylint.args": [
"--rcfile=${workspaceFolder}/pyproject.toml"
],
+1 -1
View File
@@ -820,7 +820,7 @@ jobs:
run: echo ${{ matrix.components }}
- name: Cache apt packages
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
+3
View File
@@ -123,6 +123,7 @@ esphome/components/cs5460a/* @balrog-kun
esphome/components/cse7761/* @berfenger
esphome/components/cst226/* @clydebarrow
esphome/components/cst816/* @clydebarrow
esphome/components/cst9220/* @clydebarrow
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/dac7678/* @NickB1
@@ -386,6 +387,7 @@ esphome/components/pcm5122/* @remcom
esphome/components/pi4ioe5v6408/* @jesserockz
esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984
esphome/components/pixoo/* @jesserockz
esphome/components/pm1006/* @habbie
esphome/components/pm2005/* @andrewjswan
esphome/components/pmsa003i/* @sjtrny
@@ -405,6 +407,7 @@ esphome/components/psram/* @esphome/core
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
esphome/components/pvvx_mithermometer/* @pasiz
esphome/components/pylontech/* @functionpointer
esphome/components/qmi8658/* @clydebarrow
esphome/components/qmp6988/* @andrewpc
esphome/components/qr_code/* @wjtje
esphome/components/qspi_dbi/* @clydebarrow
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.21
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.23
RUN \
platformio settings set enable_telemetry No \
+4
View File
@@ -21,6 +21,10 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
# Keep the native ESP-IDF install on the persistent cache root, not the
# container's ephemeral user cache dir (re-downloaded on every restart).
export ESPHOME_ESP_IDF_PREFIX="$(dirname "${pio_cache_base}")/idf"
# If /build is mounted, use that as the build path
# otherwise use path in /config (so that builds aren't lost on container restart)
if [[ -d /build ]]; then
@@ -15,6 +15,10 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
# Keep the native ESP-IDF install on the persistent /data volume, not the
# container's ephemeral user cache dir (wiped on every add-on update/restart).
export ESPHOME_ESP_IDF_PREFIX=/data/cache/idf
if bashio::config.true 'leave_front_door_open'; then
export DISABLE_HA_AUTHENTICATION=true
fi
+1 -1
View File
@@ -2,6 +2,6 @@ esphome:
name: docker-test-ln882x-arduino
ln882x:
board: generic-ln882hki
board: generic-ln882h
logger:
+4 -1
View File
@@ -2386,7 +2386,10 @@ def parse_args(argv):
)
parser_clean_all = subparsers.add_parser(
"clean-all", help="Clean all build and platform files."
"clean-all",
help="Clean all build and platform files, including machine-global "
"toolchain caches shared by all configurations, so other projects will "
"re-download them on next build.",
)
parser_clean_all.add_argument(
"configuration", help="Your YAML file or configuration directory.", nargs="*"
File diff suppressed because it is too large Load Diff
@@ -68,11 +68,15 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
void loop() override;
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
void register_connection(BluetoothConnection *connection) {
// maybe_unused: in a passive proxy (active: false) MAX is 0, the body below is removed, and connection is unused.
void register_connection([[maybe_unused]] BluetoothConnection *connection) {
// Guard the always-false comparison (-Wtype-limits) in a passive proxy (active: false), where MAX is 0.
#if BLUETOOTH_PROXY_MAX_CONNECTIONS > 0
if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
this->connections_[this->connection_count_++] = connection;
connection->proxy_ = this;
}
#endif
}
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
+6
View File
@@ -0,0 +1,6 @@
import esphome.codegen as cg
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["i2c"]
cst9220_ns = cg.esphome_ns.namespace("cst9220")
@@ -0,0 +1,36 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
from .. import cst9220_ns
CST9220Touchscreen = cst9220_ns.class_(
"CST9220Touchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CONFIG_SCHEMA = (
touchscreen.touchscreen_schema("100ms")
.extend(
{
cv.GenerateID(): cv.declare_id(CST9220Touchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
)
.extend(i2c.i2c_device_schema(0x5A))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
if reset_pin := config.get(CONF_RESET_PIN):
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
@@ -0,0 +1,141 @@
#include "cst9220_touchscreen.h"
#include "esphome/core/helpers.h"
#include <cinttypes>
namespace esphome::cst9220 {
void CST9220Touchscreen::setup() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(10);
this->reset_pin_->digital_write(true);
}
// Wait for the controller to leave its bootloader before talking to it.
this->set_timeout(30, [this] { this->continue_setup_(); });
}
void CST9220Touchscreen::continue_setup_() {
uint8_t buffer[4];
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
// Enter command mode so the configuration registers can be read.
if (this->write_register16(REG_CMD_MODE, buffer, 0) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to enter command mode"));
this->mark_failed();
return;
}
delay(10);
// The firmware check code confirms that valid firmware is loaded.
if (this->read_register16(REG_CHECKCODE, buffer, 4) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to read check code"));
this->mark_failed();
return;
}
uint32_t checkcode = encode_uint32(buffer[3], buffer[2], buffer[1], buffer[0]);
if ((checkcode & 0xFFFF0000) != 0xCACA0000) {
ESP_LOGE(TAG, "Invalid firmware check code: 0x%08" PRIX32, checkcode);
this->status_set_error(LOG_STR("Invalid firmware check code"));
this->mark_failed();
return;
}
// Read the panel resolution unless the user supplied calibration values.
if (this->read_register16(REG_RESOLUTION, buffer, 4) == i2c::ERROR_OK) {
if (this->x_raw_max_ == this->x_raw_min_)
this->x_raw_max_ = encode_uint16(buffer[1], buffer[0]);
if (this->y_raw_max_ == this->y_raw_min_)
this->y_raw_max_ = encode_uint16(buffer[3], buffer[2]);
}
// Read the chip type and project id and validate the controller.
if (this->read_register16(REG_CHIP_INFO, buffer, 4) != i2c::ERROR_OK) {
this->status_set_error(LOG_STR("Failed to read chip ID"));
this->mark_failed();
return;
}
this->chip_id_ = encode_uint16(buffer[3], buffer[2]);
this->project_id_ = encode_uint16(buffer[1], buffer[0]);
if (this->chip_id_ != CST9220_CHIP_ID && this->chip_id_ != CST9217_CHIP_ID) {
ESP_LOGE(TAG, "Unknown chip ID: 0x%04X", this->chip_id_);
this->status_set_error(LOG_STR("Unknown chip ID"));
this->mark_failed();
return;
}
// Fall back to the display dimensions if the resolution read failed.
if (this->x_raw_max_ == this->x_raw_min_)
this->x_raw_max_ = this->display_->get_native_width();
if (this->y_raw_max_ == this->y_raw_min_)
this->y_raw_max_ = this->display_->get_native_height();
this->setup_complete_ = true;
}
void CST9220Touchscreen::update_touches() {
if (!this->setup_complete_)
return;
uint8_t data[CST9220_DATA_LENGTH];
// Only an actual I2C failure should skip the update; a successful read with no
// touches is a real "all fingers lifted" state that must flow through so the
// base class can generate the release event.
if (this->read_register16(REG_TOUCH_DATA, data, sizeof(data)) != i2c::ERROR_OK) {
this->status_set_warning();
this->skip_update_ = true;
return;
}
this->status_clear_warning();
// Acknowledge the report so the controller can prepare the next one.
uint8_t ack = TOUCH_ACK;
this->write_register16(REG_TOUCH_DATA, &ack, 1);
// A valid report carries the ACK marker at offset 6; offset 0 holds the first
// point and must be neither the ACK marker nor empty. Anything else means no
// valid touch data this cycle, which we report as zero touches (not a skip).
if (data[0] == TOUCH_ACK || data[0] == 0x00 || data[6] != TOUCH_ACK)
return;
uint8_t num_touches = data[5] & 0x7F;
if (num_touches > CST9220_MAX_TOUCHES)
num_touches = CST9220_MAX_TOUCHES;
for (uint8_t i = 0; i < num_touches; i++) {
// The first point starts at offset 0; subsequent points are offset by the
// two status bytes that follow it.
const uint8_t *p = data + i * 5 + (i == 0 ? 0 : 2);
uint8_t id = p[0] >> 4;
uint8_t event = p[0] & 0x0F;
if (event != TOUCH_EVENT_DOWN)
continue;
// p[3] is shared: high nibble holds the X LSBs, low nibble the Y LSBs.
uint16_t x = (p[1] << 4) | (p[3] >> 4);
uint16_t y = (p[2] << 4) | (p[3] & 0x0F);
ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
this->add_raw_touch_position_(id, x, y);
}
}
void CST9220Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG,
"CST9220 Touchscreen:\n"
" Chip ID: 0x%04X\n"
" Project ID: 0x%04X\n"
" X Raw Min: %d, X Raw Max: %d\n"
" Y Raw Min: %d, Y Raw Max: %d",
this->chip_id_, this->project_id_, this->x_raw_min_, this->x_raw_max_, this->y_raw_min_,
this->y_raw_max_);
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
}
} // namespace esphome::cst9220
@@ -0,0 +1,50 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::cst9220 {
static const char *const TAG = "cst9220.touchscreen";
// The CST92xx family uses 16-bit (big-endian) register addresses.
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
static const uint16_t REG_CMD_MODE = 0xD101; // enter command mode
static const uint16_t REG_CHECKCODE = 0xD1FC; // firmware check code
static const uint16_t REG_RESOLUTION = 0xD1F8; // panel resolution
static const uint16_t REG_CHIP_INFO = 0xD204; // chip type + project id
static const uint8_t TOUCH_ACK = 0xAB;
static const uint8_t TOUCH_EVENT_DOWN = 0x06;
static const uint16_t CST9220_CHIP_ID = 0x9220;
static const uint16_t CST9217_CHIP_ID = 0x9217;
// Maximum simultaneous touch points reported by the family.
static const uint8_t CST9220_MAX_TOUCHES = 5;
// Report layout: 5 bytes per touch point plus 5 bytes of status/ack overhead.
static const size_t CST9220_DATA_LENGTH = CST9220_MAX_TOUCHES * 5 + 5;
class CST9220Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
protected:
void update_touches() override;
void continue_setup_();
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{};
uint16_t chip_id_{};
uint16_t project_id_{};
bool setup_complete_{};
};
} // namespace esphome::cst9220
+11 -2
View File
@@ -94,6 +94,15 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
}
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
// Drop oversized frames before copying. ESP-NOW v2 peers (IDF >= 5.4 builds a
// v2 stack with no opt-out) can send up to ESP_NOW_MAX_DATA_LEN_V2 (1470 B),
// but our receive buffer is ESP_NOW_MAX_DATA_LEN (250 B); copying a larger
// frame would overflow packet_.receive.data.
if (size < 0 || size > ESP_NOW_MAX_DATA_LEN) {
global_esp_now->receive_packet_queue_.increment_dropped_count();
return;
}
// Allocate an event from the pool
ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate();
if (packet == nullptr) {
@@ -327,13 +336,13 @@ void ESPNowComponent::loop() {
// Log dropped received packets periodically
uint16_t received_dropped = this->receive_packet_queue_.get_and_reset_dropped_count();
if (received_dropped > 0) {
ESP_LOGW(TAG, "Dropped %u received packets due to buffer overflow", received_dropped);
ESP_LOGW(TAG, "Dropped %u received packets (queue full or oversized frame)", received_dropped);
}
// Log dropped send packets periodically
uint16_t send_dropped = this->send_packet_queue_.get_and_reset_dropped_count();
if (send_dropped > 0) {
ESP_LOGW(TAG, "Dropped %u send packets due to buffer overflow", send_dropped);
ESP_LOGW(TAG, "Dropped %u send packets (queue full)", send_dropped);
}
}
@@ -839,7 +839,7 @@ void EthernetComponent::dump_connect_params_() {
case ETH_SPEED_100M:
link_speed = 100;
break;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 1, 0)
case ETH_SPEED_1000M:
link_speed = 1000;
break;
@@ -65,7 +65,7 @@ constexpr size_t RTU2_TODAY_PRODUCTION = 53; // length = 2
constexpr size_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2
constexpr size_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1
class GrowattSolar final : public PollingComponent, public modbus::ModbusDevice {
class GrowattSolar final : public PollingComponent, public modbus::ModbusClientDevice {
public:
void loop() override;
void update() override;
+10 -2
View File
@@ -25,6 +25,7 @@ from esphome.const import (
UNIT_VOLT,
UNIT_WATT,
)
from esphome.types import ConfigType
CONF_ENERGY_PRODUCTION_DAY = "energy_production_day"
CONF_TOTAL_ENERGY_PRODUCTION = "total_energy_production"
@@ -47,7 +48,7 @@ CODEOWNERS = ["@leeuwte"]
growatt_solar_ns = cg.esphome_ns.namespace("growatt_solar")
GrowattSolar = growatt_solar_ns.class_(
"GrowattSolar", cg.PollingComponent, modbus.ModbusDevice
"GrowattSolar", cg.PollingComponent, modbus.ModbusClientDevice
)
PHASE_SENSORS = {
@@ -162,10 +163,17 @@ CONFIG_SCHEMA = (
)
def _final_validate(config: ConfigType) -> ConfigType:
return modbus.final_validate_modbus_device("growatt_solar", role="client")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await modbus.register_modbus_device(var, config)
await modbus.register_modbus_client_device(var, config)
cg.add(var.set_protocol_version(config[CONF_PROTOCOL_VERSION]))
@@ -8,7 +8,7 @@
namespace esphome::havells_solar {
class HavellsSolar final : public PollingComponent, public modbus::ModbusDevice {
class HavellsSolar final : public PollingComponent, public modbus::ModbusClientDevice {
public:
void set_voltage_sensor(uint8_t phase, sensor::Sensor *voltage_sensor) {
this->phases_[phase].setup = true;
+10 -2
View File
@@ -28,6 +28,7 @@ from esphome.const import (
UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT,
)
from esphome.types import ConfigType
CONF_ENERGY_PRODUCTION_DAY = "energy_production_day"
CONF_TOTAL_ENERGY_PRODUCTION = "total_energy_production"
@@ -58,7 +59,7 @@ CODEOWNERS = ["@sourabhjaiswal"]
havells_solar_ns = cg.esphome_ns.namespace("havells_solar")
HavellsSolar = havells_solar_ns.class_(
"HavellsSolar", cg.PollingComponent, modbus.ModbusDevice
"HavellsSolar", cg.PollingComponent, modbus.ModbusClientDevice
)
PHASE_SENSORS = {
@@ -216,10 +217,17 @@ CONFIG_SCHEMA = (
)
def _final_validate(config: ConfigType) -> ConfigType:
return modbus.final_validate_modbus_device("havells_solar", role="client")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await modbus.register_modbus_device(var, config)
await modbus.register_modbus_client_device(var, config)
if CONF_FREQUENCY in config:
sens = await sensor.new_sensor(config[CONF_FREQUENCY])
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::kuntze {
class Kuntze final : public PollingComponent, public modbus::ModbusDevice {
class Kuntze final : public PollingComponent, public modbus::ModbusClientDevice {
public:
void set_ph_sensor(sensor::Sensor *ph_sensor) { ph_sensor_ = ph_sensor; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
+10 -2
View File
@@ -15,13 +15,14 @@ from esphome.const import (
UNIT_EMPTY,
UNIT_PH,
)
from esphome.types import ConfigType
CODEOWNERS = ["@ssieb"]
AUTO_LOAD = ["modbus"]
kuntze_ns = cg.esphome_ns.namespace("kuntze")
Kuntze = kuntze_ns.class_("Kuntze", cg.PollingComponent, modbus.ModbusDevice)
Kuntze = kuntze_ns.class_("Kuntze", cg.PollingComponent, modbus.ModbusClientDevice)
CONF_DIS1 = "dis1"
CONF_DIS2 = "dis2"
@@ -88,10 +89,17 @@ CONFIG_SCHEMA = (
)
def _final_validate(config: ConfigType) -> ConfigType:
return modbus.final_validate_modbus_device("kuntze", role="client")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await modbus.register_modbus_device(var, config)
await modbus.register_modbus_client_device(var, config)
if CONF_PH in config:
conf = config[CONF_PH]
+5 -5
View File
@@ -211,14 +211,14 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters.
# Use GitHub releases directly to avoid PlatformIO moderation delays.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
"dev": (cv.Version(1, 13, 0), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
cv.Version(1, 13, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.13.0",
),
"recommended": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
cv.Version(1, 13, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.13.0",
),
}
@@ -359,7 +359,9 @@ if __name__ == "__main__":
check_base_code(BASE_CODE_INIT)
# list all boards from ltchiptool
components_dir = Path(__file__).parent.parent
boards = [Board(b) for b in Board.get_list()]
# Board.get_list() returns glob (filesystem) order, which is non-deterministic
# and produces noisy diffs on regeneration; sort by board id for stable output.
boards = sorted((Board(b) for b in Board.get_list()), key=lambda b: b.name)
# keep track of all supported root- and chip-families
components = set()
families = {}
+412 -85
View File
@@ -15,26 +15,38 @@ Any manual changes WILL BE LOST on regeneration.
from esphome.components.libretiny.const import FAMILY_LN882H
LN882X_BOARDS = {
"generic-ln882hki": {
"name": "Generic - LN882HKI",
"generic-ln882h": {
"name": "Generic - LN882H",
"family": FAMILY_LN882H,
},
"wb02a": {
"name": "WB02A Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"wl2s": {
"name": "WL2S Wi-Fi/BLE Module",
"generic-ln882h-tuya": {
"name": "Generic - LN882H (Tuya)",
"family": FAMILY_LN882H,
},
"ln-02": {
"name": "LN-02 Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"ln-cb3s-v1.0": {
"name": "LN-CB3S V1.0",
"family": FAMILY_LN882H,
},
"wb02a": {
"name": "WB02A Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"wl2h-u": {
"name": "WL2H-U Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
"wl2s": {
"name": "WL2S Wi-Fi/BLE Module",
"family": FAMILY_LN882H,
},
}
LN882X_BOARD_PINS = {
"generic-ln882hki": {
"generic-ln882h": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
@@ -153,27 +165,292 @@ LN882X_BOARD_PINS = {
"A6": 20,
"A7": 21,
},
"generic-ln882h-tuya": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 4,
"WIRE0_SCL_5": 5,
"WIRE0_SCL_6": 6,
"WIRE0_SCL_7": 7,
"WIRE0_SCL_8": 8,
"WIRE0_SCL_9": 9,
"WIRE0_SCL_10": 10,
"WIRE0_SCL_11": 11,
"WIRE0_SCL_12": 12,
"WIRE0_SCL_13": 19,
"WIRE0_SCL_14": 20,
"WIRE0_SCL_15": 21,
"WIRE0_SCL_16": 22,
"WIRE0_SCL_17": 23,
"WIRE0_SCL_18": 24,
"WIRE0_SCL_19": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 4,
"WIRE0_SDA_5": 5,
"WIRE0_SDA_6": 6,
"WIRE0_SDA_7": 7,
"WIRE0_SDA_8": 8,
"WIRE0_SDA_9": 9,
"WIRE0_SDA_10": 10,
"WIRE0_SDA_11": 11,
"WIRE0_SDA_12": 12,
"WIRE0_SDA_13": 19,
"WIRE0_SDA_14": 20,
"WIRE0_SDA_15": 21,
"WIRE0_SDA_16": 22,
"WIRE0_SDA_17": 23,
"WIRE0_SDA_18": 24,
"WIRE0_SDA_19": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC4": 4,
"ADC5": 19,
"ADC6": 20,
"ADC7": 21,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA07": 7,
"PA7": 7,
"PA08": 8,
"PA8": 8,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PB03": 19,
"PB3": 19,
"PB04": 20,
"PB4": 20,
"PB05": 21,
"PB5": 21,
"PB06": 22,
"PB6": 22,
"PB07": 23,
"PB7": 23,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"TX0": 2,
"TX1": 25,
"D0": 0,
"D1": 1,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 5,
"D6": 6,
"D7": 7,
"D8": 8,
"D9": 9,
"D10": 10,
"D11": 11,
"D12": 12,
"D13": 19,
"D14": 20,
"D15": 21,
"D16": 22,
"D17": 23,
"D18": 24,
"D19": 25,
"A2": 0,
"A3": 1,
"A4": 4,
"A5": 19,
"A6": 20,
"A7": 21,
},
"ln-02": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 9,
"WIRE0_SCL_5": 11,
"WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 24,
"WIRE0_SCL_8": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 9,
"WIRE0_SDA_5": 11,
"WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 24,
"WIRE0_SDA_8": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC5": 19,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA09": 9,
"PA9": 9,
"PA11": 11,
"PB03": 19,
"PB3": 19,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"SCL0": 9,
"SDA0": 9,
"TX0": 2,
"TX1": 25,
"D0": 11,
"D1": 19,
"D2": 3,
"D3": 24,
"D4": 2,
"D5": 25,
"D6": 1,
"D7": 0,
"D8": 9,
"A0": 19,
"A1": 1,
"A2": 0,
},
"ln-cb3s-v1.0": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 4,
"WIRE0_SCL_5": 5,
"WIRE0_SCL_6": 6,
"WIRE0_SCL_7": 9,
"WIRE0_SCL_8": 11,
"WIRE0_SCL_9": 20,
"WIRE0_SCL_10": 21,
"WIRE0_SCL_11": 22,
"WIRE0_SCL_12": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 4,
"WIRE0_SDA_5": 5,
"WIRE0_SDA_6": 6,
"WIRE0_SDA_7": 9,
"WIRE0_SDA_8": 11,
"WIRE0_SDA_9": 20,
"WIRE0_SDA_10": 21,
"WIRE0_SDA_11": 22,
"WIRE0_SDA_12": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC4": 4,
"ADC6": 20,
"ADC7": 21,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA09": 9,
"PA9": 9,
"PA11": 11,
"PB04": 20,
"PB4": 20,
"PB05": 21,
"PB5": 21,
"PB06": 22,
"PB6": 22,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"TX0": 2,
"TX1": 25,
"D0": 0,
"D1": 1,
"D2": 4,
"D3": 5,
"D4": 6,
"D5": 20,
"D6": 25,
"D7": 9,
"D8": 21,
"D9": 22,
"D10": 3,
"D11": 2,
"D12": 11,
"A0": 0,
"A1": 1,
"A2": 4,
"A3": 20,
"A4": 21,
},
"wb02a": {
"WIRE0_SCL_0": 1,
"WIRE0_SCL_1": 2,
"WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 4,
"WIRE0_SCL_4": 5,
"WIRE0_SCL_5": 7,
"WIRE0_SCL_6": 9,
"WIRE0_SCL_7": 10,
"WIRE0_SCL_8": 24,
"WIRE0_SCL_9": 25,
"WIRE0_SCL_5": 6,
"WIRE0_SCL_6": 7,
"WIRE0_SCL_7": 9,
"WIRE0_SCL_8": 10,
"WIRE0_SCL_9": 24,
"WIRE0_SCL_10": 25,
"WIRE0_SDA_0": 1,
"WIRE0_SDA_1": 2,
"WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 4,
"WIRE0_SDA_4": 5,
"WIRE0_SDA_5": 7,
"WIRE0_SDA_6": 9,
"WIRE0_SDA_7": 10,
"WIRE0_SDA_8": 24,
"WIRE0_SDA_9": 25,
"WIRE0_SDA_5": 6,
"WIRE0_SDA_6": 7,
"WIRE0_SDA_7": 9,
"WIRE0_SDA_8": 10,
"WIRE0_SDA_9": 24,
"WIRE0_SDA_10": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
@@ -190,6 +467,8 @@ LN882X_BOARD_PINS = {
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA07": 7,
"PA7": 7,
"PA09": 9,
@@ -206,18 +485,128 @@ LN882X_BOARD_PINS = {
"TX0": 2,
"TX1": 25,
"D0": 7,
"D1": 5,
"D1": 6,
"D2": 3,
"D3": 10,
"D4": 2,
"D5": 1,
"D6": 4,
"D7": 9,
"D8": 24,
"D9": 25,
"D7": 5,
"D8": 9,
"D9": 24,
"D10": 25,
"A0": 1,
"A1": 4,
},
"wl2h-u": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 4,
"WIRE0_SCL_5": 5,
"WIRE0_SCL_6": 6,
"WIRE0_SCL_7": 7,
"WIRE0_SCL_8": 10,
"WIRE0_SCL_9": 11,
"WIRE0_SCL_10": 12,
"WIRE0_SCL_11": 19,
"WIRE0_SCL_12": 20,
"WIRE0_SCL_13": 21,
"WIRE0_SCL_14": 22,
"WIRE0_SCL_15": 23,
"WIRE0_SCL_16": 24,
"WIRE0_SCL_17": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 4,
"WIRE0_SDA_5": 5,
"WIRE0_SDA_6": 6,
"WIRE0_SDA_7": 7,
"WIRE0_SDA_8": 10,
"WIRE0_SDA_9": 11,
"WIRE0_SDA_10": 12,
"WIRE0_SDA_11": 19,
"WIRE0_SDA_12": 20,
"WIRE0_SDA_13": 21,
"WIRE0_SDA_14": 22,
"WIRE0_SDA_15": 23,
"WIRE0_SDA_16": 24,
"WIRE0_SDA_17": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC4": 4,
"ADC5": 19,
"ADC6": 20,
"ADC7": 21,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA05": 5,
"PA5": 5,
"PA06": 6,
"PA6": 6,
"PA07": 7,
"PA7": 7,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PB03": 19,
"PB3": 19,
"PB04": 20,
"PB4": 20,
"PB05": 21,
"PB5": 21,
"PB06": 22,
"PB6": 22,
"PB07": 23,
"PB7": 23,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"TX0": 2,
"TX1": 25,
"D0": 5,
"D1": 6,
"D2": 4,
"D3": 1,
"D4": 0,
"D5": 24,
"D6": 25,
"D7": 7,
"D8": 10,
"D9": 11,
"D10": 12,
"D11": 19,
"D12": 2,
"D13": 3,
"D14": 20,
"D15": 21,
"D16": 22,
"D17": 23,
"A0": 4,
"A1": 1,
"A2": 0,
"A3": 19,
"A4": 20,
"A5": 21,
},
"wl2s": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
@@ -298,68 +687,6 @@ LN882X_BOARD_PINS = {
"A1": 19,
"A2": 1,
},
"ln-02": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 9,
"WIRE0_SCL_5": 11,
"WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 24,
"WIRE0_SCL_8": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 9,
"WIRE0_SDA_5": 11,
"WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 24,
"WIRE0_SDA_8": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
"SERIAL1_TX": 25,
"ADC2": 0,
"ADC3": 1,
"ADC5": 19,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA09": 9,
"PA9": 9,
"PA11": 11,
"PB03": 19,
"PB3": 19,
"PB08": 24,
"PB8": 24,
"PB09": 25,
"PB9": 25,
"RX0": 3,
"RX1": 24,
"SCL0": 9,
"SDA0": 9,
"TX0": 2,
"TX1": 25,
"D0": 11,
"D1": 19,
"D2": 3,
"D3": 24,
"D4": 2,
"D5": 25,
"D6": 1,
"D7": 0,
"D8": 9,
"A0": 19,
"A1": 1,
"A2": 0,
},
}
BOARDS = LN882X_BOARDS
+2
View File
@@ -425,6 +425,8 @@ async def to_code(config):
dc_pin = await cg.gpio_pin_expression(dc_pin)
cg.add(var.set_dc_pin(dc_pin))
if config.get(CONF_INVERT_COLORS):
cg.add(var.set_invert_colors(True))
if lamb := config.get(CONF_LAMBDA):
lambda_ = await cg.process_lambda(
lamb, [(display.DisplayRef, "it")], return_type=cg.void
+3
View File
@@ -151,6 +151,9 @@ class MipiSpi : public display::Display,
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
} else {
// no reset pin, send software reset command
this->write_command_(SW_RESET_CMD);
}
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
+1 -6
View File
@@ -24,13 +24,11 @@ from esphome.components.mipi import (
PWSET,
PWSETN,
SETEXTC,
SWRESET,
VMCTR,
VMCTR1,
VMCTR2,
VSCRSADD,
DriverChip,
delay,
)
from esphome.components.spi import TYPE_OCTAL
@@ -367,7 +365,6 @@ ST7796 = DriverChip(
width=320,
height=480,
initsequence=(
(SWRESET,),
(CSCON, 0xC3),
(CSCON, 0x96),
(VMCTR1, 0x1C),
@@ -728,8 +725,6 @@ DriverChip(
width=128,
height=160,
initsequence=(
SWRESET,
delay(10),
(FRMCTR1, 0x01, 0x2C, 0x2D),
(FRMCTR2, 0x01, 0x2C, 0x2D),
(FRMCTR3, 0x01, 0x2C, 0x2D, 0x01, 0x2C, 0x2D),
@@ -786,7 +781,7 @@ ST7796.extend(
bus_mode=TYPE_OCTAL,
mirror_x=True,
reset_pin=4,
dc_pin=0,
dc_pin={"number": 0, "ignore_strapping_warning": True},
invert_colors=True,
)
+8
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
from typing import Literal
from esphome import pins
@@ -10,6 +11,8 @@ from esphome.const import CONF_ADDRESS, CONF_DISABLE_CRC, CONF_FLOW_CONTROL_PIN,
from esphome.cpp_helpers import gpio_pin_expression
import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["uart"]
modbus_ns = cg.esphome_ns.namespace("modbus")
@@ -129,4 +132,9 @@ async def register_modbus_server_device(var, config):
async def register_modbus_device(var, config):
# Remove before 2026.12.0
_LOGGER.warning(
"'register_modbus_device' is deprecated, use 'register_modbus_client_device' "
"instead. Will be removed in 2026.12.0"
)
return await register_modbus_client_device(var, config)
+3 -1
View File
@@ -197,7 +197,9 @@ class ModbusClientDevice {
};
// This is for compatibility with external components using the former class name
using ModbusDevice = ModbusClientDevice;
// Remove before 2026.12.0
using ModbusDevice ESPDEPRECATED("Use ModbusClientDevice instead. Removed in 2026.12.0",
"2026.6.0") = ModbusClientDevice;
// Result of a server register handler: std::nullopt means success, otherwise the Modbus exception code to return.
using ServerResponseStatus = std::optional<ModbusExceptionCode>;
@@ -11,6 +11,7 @@ from esphome.components.modbus.helpers import (
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_NAME, CONF_OFFSET
from esphome.cpp_helpers import logging
from esphome.types import ConfigType
from .const import (
CONF_ALLOW_DUPLICATE_COMMANDS,
@@ -42,7 +43,7 @@ MULTI_CONF = True
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
ModbusController = modbus_controller_ns.class_(
"ModbusController", cg.PollingComponent, modbus.ModbusDevice
"ModbusController", cg.PollingComponent, modbus.ModbusClientDevice
)
SensorItem = modbus_controller_ns.struct("SensorItem")
@@ -117,7 +118,7 @@ def validate_modbus_register(config):
return config
def _final_validate(config):
def _final_validate(config: ConfigType) -> ConfigType:
return modbus.final_validate_modbus_device("modbus_controller", role="client")(
config
)
@@ -211,7 +212,7 @@ async def to_code(config):
async def register_modbus_device(var, config):
cg.add(var.set_address(config[CONF_ADDRESS]))
await cg.register_component(var, config)
return await modbus.register_modbus_device(var, config)
return await modbus.register_modbus_client_device(var, config)
def function_code_to_register(function_code):
+10 -1
View File
@@ -57,6 +57,7 @@ from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
PlatformFramework,
)
@@ -318,7 +319,15 @@ CONFIG_SCHEMA = cv.All(
}
),
validate_config,
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]),
cv.only_on(
[
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
]
),
_consume_mqtt_sockets,
)
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@jesserockz"]
+43
View File
@@ -0,0 +1,43 @@
import esphome.codegen as cg
from esphome.components import display, spi
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_MODEL
from esphome.types import ConfigType
DEPENDENCIES = ["spi"]
AUTO_LOAD = ["split_buffer"]
CONF_PIXOO_ID = "pixoo_id"
pixoo_ns = cg.esphome_ns.namespace("pixoo")
Pixoo = pixoo_ns.class_("Pixoo", cg.PollingComponent, display.Display, spi.SPIDevice)
PixooModel = pixoo_ns.enum("PixooModel")
# Only the 64x64 panel is hardware-verified. Smaller Pixoo panels are assumed to share the
# same protocol; add them here once confirmed.
MODELS = {
"64X64": PixooModel.PIXOO_64,
}
CONFIG_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(Pixoo),
cv.Optional(CONF_MODEL, default="64X64"): cv.enum(MODELS, upper=True),
}
).extend(spi.spi_device_schema(cs_pin_required=True, default_data_rate=8e6))
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
"pixoo", require_miso=False, require_mosi=True
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID], config[CONF_MODEL])
await display.register_display(var, config)
await spi.register_spi_device(var, config, write_only=True)
if (lambda_config := config.get(CONF_LAMBDA)) is not None:
lambda_ = await cg.process_lambda(
lambda_config, [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
@@ -0,0 +1,24 @@
import esphome.codegen as cg
from esphome.components import light
import esphome.config_validation as cv
from esphome.const import CONF_GAMMA_CORRECT, CONF_OUTPUT_ID
from esphome.types import ConfigType
from ..display import CONF_PIXOO_ID, Pixoo, pixoo_ns
PixooLight = pixoo_ns.class_("PixooLight", light.LightOutput)
CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(PixooLight),
cv.GenerateID(CONF_PIXOO_ID): cv.use_id(Pixoo),
# The LED board applies its own gamma, so default to no gamma correction here.
cv.Optional(CONF_GAMMA_CORRECT, default=0.0): cv.positive_float,
}
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await light.register_light(var, config)
await cg.register_parented(var, config[CONF_PIXOO_ID])
@@ -0,0 +1,26 @@
#pragma once
#include "esphome/components/light/light_output.h"
#include "esphome/components/light/light_state.h"
#include "esphome/components/pixoo/pixoo.h"
#include "esphome/core/helpers.h"
namespace esphome::pixoo {
// Brightness-only light that drives the Pixoo panel's LIGHT command.
class PixooLight : public light::LightOutput, public Parented<Pixoo> {
public:
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
return traits;
}
void write_state(light::LightState *state) override {
float brightness;
state->current_values_as_brightness(&brightness);
this->parent_->set_panel_brightness(brightness);
}
};
} // namespace esphome::pixoo
+201
View File
@@ -0,0 +1,201 @@
#include "pixoo.h"
#include "esphome/core/log.h"
#include <cmath>
#include <cstring>
#include <utility>
namespace esphome::pixoo {
static const char *const TAG = "pixoo";
// Divoom LED-board packet protocol.
static constexpr uint8_t PACKET_HEAD = 0xAA;
static constexpr uint8_t PACKET_TAIL = 0xBB;
static constexpr uint8_t CMD_DATA = 0x00;
static constexpr uint8_t CMD_LIGHT = 0x01;
static constexpr uint8_t CMD_UNUSED = 0x21;
static constexpr uint8_t CMD_SET_RGB_IOUT = 0x22;
static constexpr size_t PACKET_HEADER_LEN = 4; // head + len(2) + cmd
static constexpr size_t PACKET_STATIC_LEN = 5; // header + tail
static constexpr uint8_t DEFAULT_IOUT = 75; // per-channel LED current / white balance default
// Pack a `0xAA len cmd data 0xBB` packet into buf; returns the packet length.
static inline size_t build_packet(uint8_t *buf, uint8_t cmd, const uint8_t *data, uint16_t len) {
buf[0] = PACKET_HEAD;
buf[1] = static_cast<uint8_t>(len & 0xFF);
buf[2] = static_cast<uint8_t>((len >> 8) & 0xFF);
buf[3] = cmd;
if (data != nullptr && len > 0)
std::memcpy(buf + PACKET_HEADER_LEN, data, len);
buf[PACKET_HEADER_LEN + len] = PACKET_TAIL;
return len + PACKET_STATIC_LEN;
}
// Fill `total` bytes at buf with a single UNUSED padding packet.
static inline void pad_unused(uint8_t *buf, size_t total) {
const uint16_t len = static_cast<uint16_t>(total - PACKET_STATIC_LEN);
buf[0] = PACKET_HEAD;
buf[1] = static_cast<uint8_t>(len & 0xFF);
buf[2] = static_cast<uint8_t>((len >> 8) & 0xFF);
buf[3] = CMD_UNUSED;
buf[total - 1] = PACKET_TAIL;
}
float Pixoo::get_setup_priority() const { return setup_priority::PROCESSOR; }
void Pixoo::setup() {
const uint32_t num_pixels = static_cast<uint32_t>(this->model_) * this->model_;
this->data_size_ = num_pixels * 3;
// The frame is a DATA packet (header + RGB888 + tail) followed by a DMA-chunk-sized UNUSED
// packet, so the LED board completes its final DMA block.
this->frame_size_ = this->data_size_ + PACKET_STATIC_LEN + DMA_CHUNK;
if (!this->buffer_.init(this->data_size_)) {
this->mark_failed(LOG_STR("Failed to allocate draw buffer"));
return;
}
// The frame is shipped in one SPI transfer, so keep it in DMA-capable internal RAM.
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
this->frame_buffer_ = allocator.allocate(this->frame_size_);
if (this->frame_buffer_ == nullptr) {
this->buffer_.free();
this->mark_failed(LOG_STR("Failed to allocate frame buffer"));
return;
}
std::memset(this->frame_buffer_, 0, this->frame_size_);
// Pre-build the constant DATA-packet framing; only the RGB888 payload changes per frame.
this->frame_buffer_[0] = PACKET_HEAD;
this->frame_buffer_[1] = static_cast<uint8_t>(this->data_size_ & 0xFF);
this->frame_buffer_[2] = static_cast<uint8_t>((this->data_size_ >> 8) & 0xFF);
this->frame_buffer_[3] = CMD_DATA;
this->frame_buffer_[PACKET_HEADER_LEN + this->data_size_] = PACKET_TAIL;
pad_unused(this->frame_buffer_ + this->data_size_ + PACKET_STATIC_LEN, DMA_CHUNK);
this->spi_setup();
this->buffer_.fill(0x00);
// Set the per-channel LED current. Brightness is controlled separately via the light platform.
const uint8_t iout[3] = {DEFAULT_IOUT, DEFAULT_IOUT, DEFAULT_IOUT};
this->send_command_(CMD_SET_RGB_IOUT, iout, 3);
// Frames are pushed synchronously inside update(), so there is no loop() work to do and the
// component is idle between updates. Marking it done (LOOP_DONE) lets LVGL's
// update_when_display_idle option treat the panel as idle and drive frames on demand.
this->disable_loop();
}
void Pixoo::send_command_(uint8_t cmd, const uint8_t *data, uint16_t len) {
std::memset(this->cmd_buffer_, 0, DMA_CHUNK);
const size_t used = build_packet(this->cmd_buffer_, cmd, data, len);
if (DMA_CHUNK - used >= PACKET_STATIC_LEN)
pad_unused(this->cmd_buffer_ + used, DMA_CHUNK - used);
this->enable();
this->write_array(this->cmd_buffer_, DMA_CHUNK);
this->disable();
}
void Pixoo::set_panel_brightness(float brightness) {
const uint8_t pct = static_cast<uint8_t>(lroundf(clamp(brightness, 0.0f, 1.0f) * 100.0f));
this->send_command_(CMD_LIGHT, &pct, 1);
}
void Pixoo::update() {
this->do_update_();
for (size_t i = 0; i < this->data_size_; i++)
this->frame_buffer_[PACKET_HEADER_LEN + i] = this->buffer_[i];
this->enable();
this->write_array(this->frame_buffer_, this->frame_size_);
this->disable();
}
void Pixoo::set_pixel_(uint32_t index, Color color) {
const size_t off = static_cast<size_t>(index) * 3;
this->buffer_[off] = color.r;
this->buffer_[off + 1] = color.g;
this->buffer_[off + 2] = color.b;
}
void HOT Pixoo::draw_pixel_at(int x, int y, Color color) {
if (!this->get_clipping().inside(x, y))
return;
const int side = static_cast<int>(this->model_);
switch (this->rotation_) {
case display::DISPLAY_ROTATION_0_DEGREES:
break;
case display::DISPLAY_ROTATION_90_DEGREES:
std::swap(x, y);
x = side - x - 1;
break;
case display::DISPLAY_ROTATION_180_DEGREES:
x = side - x - 1;
y = side - y - 1;
break;
case display::DISPLAY_ROTATION_270_DEGREES:
std::swap(x, y);
y = side - y - 1;
break;
}
if (x < 0 || x >= side || y < 0 || y >= side)
return;
this->set_pixel_(static_cast<uint32_t>(y) * side + x, color);
}
void Pixoo::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
// Fast path for the common LVGL/image blit: RGB565, RGB order, no rotation, no active clipping.
// Anything else defers to the base implementation, which decodes per pixel and routes through
// draw_pixel_at() so rotation, clipping and other color formats stay correct.
// NOTE: the stride/index math and 565->888 expansion below mirror Display::draw_pixels_at (the
// source of truth) -- keep them in sync if the base ever changes its source layout or decoding.
if (bitness != display::COLOR_BITNESS_565 || order != display::COLOR_ORDER_RGB ||
this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || this->is_clipping()) {
display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset,
x_pad);
return;
}
const int side = static_cast<int>(this->model_);
const size_t line_stride = static_cast<size_t>(x_offset) + w + x_pad;
for (int y = 0; y != h; y++) {
const int dst_y = y_start + y;
if (dst_y < 0 || dst_y >= side)
continue;
size_t source_idx = (static_cast<size_t>(y_offset) + y) * line_stride + x_offset;
for (int x = 0; x != w; x++, source_idx++) {
const int dst_x = x_start + x;
if (dst_x < 0 || dst_x >= side)
continue;
const size_t byte_idx = source_idx * 2;
const uint16_t rgb565 =
big_endian ? (ptr[byte_idx] << 8) | ptr[byte_idx + 1] : ptr[byte_idx] | (ptr[byte_idx + 1] << 8);
const uint8_t r5 = (rgb565 >> 11) & 0x1F;
const uint8_t g6 = (rgb565 >> 5) & 0x3F;
const uint8_t b5 = rgb565 & 0x1F;
this->set_pixel_(static_cast<uint32_t>(dst_y) * side + dst_x,
Color((r5 << 3) | (r5 >> 2), (g6 << 2) | (g6 >> 4), (b5 << 3) | (b5 >> 2)));
}
}
}
void Pixoo::fill(Color color) {
if (this->is_clipping()) {
display::Display::fill(color);
return;
}
for (size_t i = 0; i < this->data_size_; i += 3) {
this->buffer_[i] = color.r;
this->buffer_[i + 1] = color.g;
this->buffer_[i + 2] = color.b;
}
}
void Pixoo::dump_config() {
LOG_DISPLAY("", "Divoom Pixoo", this);
ESP_LOGCONFIG(TAG, " Model: %ux%u", (unsigned) this->model_, (unsigned) this->model_);
LOG_UPDATE_INTERVAL(this);
}
} // namespace esphome::pixoo
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include "esphome/components/display/display.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/split_buffer/split_buffer.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome::pixoo {
// The Pixoo's main board (where ESPHome runs) talks to a separate LED-driver board (a GD32/AT32
// MCU) over SPI using Divoom's packet protocol:
// 0xAA, len_lo, len_hi, cmd, <data...>, 0xBB
// The image is sent as a DATA (0x00) packet carrying width*height*3 bytes of RGB888; brightness is
// a separate LIGHT (0x01) command; the LED current is set once via SET_RGB_IOUT (0x22). Command
// packets are padded out to the LED board's 240-byte DMA chunk with an UNUSED (0x21) packet.
// The model selects the (square) panel side length.
enum PixooModel : uint8_t {
PIXOO_64 = 64,
};
class Pixoo : public display::Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
public:
explicit Pixoo(PixooModel model) : model_(model) {}
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override;
// Brightness is controlled exclusively via the light platform: send a LIGHT command to the LED
// board (brightness 0..1 -> 0..100%).
void set_panel_brightness(float brightness);
display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }
void fill(Color color) override;
void draw_pixel_at(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
protected:
int get_width_internal() override { return static_cast<int>(this->model_); }
int get_height_internal() override { return static_cast<int>(this->model_); }
void set_pixel_(uint32_t index, Color color);
void send_command_(uint8_t cmd, const uint8_t *data, uint16_t len);
// Size of the LED board's SPI DMA chunk; the command scratch buffer is one chunk.
static constexpr size_t DMA_CHUNK = 240;
PixooModel model_;
size_t data_size_{0}; // RGB888 image bytes: model^2 * 3
size_t frame_size_{0}; // full SPI frame: DATA packet + trailing UNUSED packet
split_buffer::SplitBuffer buffer_{};
uint8_t *frame_buffer_{nullptr};
uint8_t cmd_buffer_[DMA_CHUNK]{};
};
} // namespace esphome::pixoo
+1 -1
View File
@@ -11,7 +11,7 @@ namespace esphome::pzemac {
template<typename... Ts> class ResetEnergyAction;
class PZEMAC final : public PollingComponent, public modbus::ModbusDevice {
class PZEMAC final : public PollingComponent, public modbus::ModbusClientDevice {
public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
+10 -2
View File
@@ -26,11 +26,12 @@ from esphome.const import (
UNIT_WATT,
UNIT_WATT_HOURS,
)
from esphome.types import ConfigType
AUTO_LOAD = ["modbus"]
pzemac_ns = cg.esphome_ns.namespace("pzemac")
PZEMAC = pzemac_ns.class_("PZEMAC", cg.PollingComponent, modbus.ModbusDevice)
PZEMAC = pzemac_ns.class_("PZEMAC", cg.PollingComponent, modbus.ModbusClientDevice)
# Actions
ResetEnergyAction = pzemac_ns.class_("ResetEnergyAction", automation.Action)
@@ -97,10 +98,17 @@ async def reset_energy_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg, paren)
def _final_validate(config: ConfigType) -> ConfigType:
return modbus.final_validate_modbus_device("pzemac", role="client")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await modbus.register_modbus_device(var, config)
await modbus.register_modbus_client_device(var, config)
if CONF_VOLTAGE in config:
conf = config[CONF_VOLTAGE]
+1 -1
View File
@@ -9,7 +9,7 @@
namespace esphome::pzemdc {
class PZEMDC final : public PollingComponent, public modbus::ModbusDevice {
class PZEMDC final : public PollingComponent, public modbus::ModbusClientDevice {
public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
+10 -2
View File
@@ -20,11 +20,12 @@ from esphome.const import (
UNIT_VOLT,
UNIT_WATT,
)
from esphome.types import ConfigType
AUTO_LOAD = ["modbus"]
pzemdc_ns = cg.esphome_ns.namespace("pzemdc")
PZEMDC = pzemdc_ns.class_("PZEMDC", cg.PollingComponent, modbus.ModbusDevice)
PZEMDC = pzemdc_ns.class_("PZEMDC", cg.PollingComponent, modbus.ModbusClientDevice)
# Actions
ResetEnergyAction = pzemdc_ns.class_("ResetEnergyAction", automation.Action)
@@ -79,10 +80,17 @@ async def reset_energy_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg, paren)
def _final_validate(config: ConfigType) -> ConfigType:
return modbus.final_validate_modbus_device("pzemdc", role="client")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await modbus.register_modbus_device(var, config)
await modbus.register_modbus_client_device(var, config)
if CONF_VOLTAGE in config:
conf = config[CONF_VOLTAGE]
+13
View File
@@ -0,0 +1,13 @@
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.motion import MotionComponent
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["i2c", "motion"]
CONF_QMI8658_ID = "qmi8658_id"
# C++ namespace / class
qmi8658_ns = cg.esphome_ns.namespace("qmi8658")
QMI8658Component = qmi8658_ns.class_("QMI8658Component", MotionComponent, i2c.I2CDevice)
CONFIG_SCHEMA = {}
+93
View File
@@ -0,0 +1,93 @@
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.const import (
CONF_ACCELEROMETER_ODR,
CONF_ACCELEROMETER_RANGE,
CONF_GYROSCOPE_ODR,
CONF_GYROSCOPE_RANGE,
)
from esphome.components.motion import motion_schema, new_motion_component
import esphome.config_validation as cv
from . import QMI8658Component, qmi8658_ns
# Enum proxies (must match the C++ enum values exactly)
QMI8658AccelRange = qmi8658_ns.enum("QMI8658AccelRange")
ACCEL_RANGE_OPTIONS = {
"2G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_2G,
"4G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_4G,
"8G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_8G,
"16G": QMI8658AccelRange.QMI8658_ACCEL_RANGE_16G,
}
QMI8658GyroRange = qmi8658_ns.enum("QMI8658GyroRange")
GYRO_RANGE_OPTIONS = {
"16DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_16,
"32DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_32,
"64DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_64,
"128DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_128,
"256DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_256,
"512DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_512,
"1024DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_1024,
"2048DPS": QMI8658GyroRange.QMI8658_GYRO_RANGE_2048,
}
QMI8658AccelODR = qmi8658_ns.enum("QMI8658AccelODR")
ACCEL_ODR_OPTIONS = {
"31_25HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_31_25,
"62_5HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_62_5,
"125HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_125,
"250HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_250,
"500HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_500,
"1000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_1000,
"2000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_2000,
"4000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_4000,
"8000HZ": QMI8658AccelODR.QMI8658_ACCEL_ODR_8000,
}
QMI8658GyroODR = qmi8658_ns.enum("QMI8658GyroODR")
GYRO_ODR_OPTIONS = {
"31_25HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_31_25,
"62_5HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_62_5,
"125HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_125,
"250HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_250,
"500HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_500,
"1000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_1000,
"2000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_2000,
"4000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_4000,
"8000HZ": QMI8658GyroODR.QMI8658_GYRO_ODR_8000,
}
# Top-level CONFIG_SCHEMA
CONFIG_SCHEMA = (
motion_schema(QMI8658Component, has_accel=True, has_gyro=True)
.extend(
{
cv.Optional(CONF_ACCELEROMETER_RANGE, default="4G"): cv.enum(
ACCEL_RANGE_OPTIONS, upper=True
),
cv.Optional(CONF_ACCELEROMETER_ODR, default="1000HZ"): cv.enum(
ACCEL_ODR_OPTIONS, upper=True
),
cv.Optional(CONF_GYROSCOPE_RANGE, default="2048DPS"): cv.enum(
GYRO_RANGE_OPTIONS, upper=True
),
cv.Optional(CONF_GYROSCOPE_ODR, default="1000HZ"): cv.enum(
GYRO_ODR_OPTIONS, upper=True
),
}
)
.extend(i2c.i2c_device_schema(0x6B))
)
# Code generation
async def to_code(config):
var = await new_motion_component(config)
await i2c.register_i2c_device(var, config)
# Hardware configuration
cg.add(var.set_accel_range(config[CONF_ACCELEROMETER_RANGE]))
cg.add(var.set_accel_odr(config[CONF_ACCELEROMETER_ODR]))
cg.add(var.set_gyro_range(config[CONF_GYROSCOPE_RANGE]))
cg.add(var.set_gyro_odr(config[CONF_GYROSCOPE_ODR]))
+136
View File
@@ -0,0 +1,136 @@
#include "qmi8658.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome::qmi8658 {
static const char *const TAG = "qmi8658";
// Acceleration scale (g per LSB), indexed by accel_range_ >> 4.
// Full-scale = range_g, mapped over a signed 16-bit value (2^15 counts).
static constexpr float ACCEL_SCALE[] = {
2.0f / 32768.0f,
4.0f / 32768.0f,
8.0f / 32768.0f,
16.0f / 32768.0f,
};
// Angular rate scale (°/s per LSB), indexed by gyro_range_ >> 4.
static constexpr float GYRO_SCALE[] = {
16.0f / 32768.0f, 32.0f / 32768.0f, 64.0f / 32768.0f, 128.0f / 32768.0f,
256.0f / 32768.0f, 512.0f / 32768.0f, 1024.0f / 32768.0f, 2048.0f / 32768.0f,
};
void QMI8658Component::setup() {
MotionComponent::setup();
// 1. Verify chip ID
uint8_t who_am_i = 0;
if (!this->read_byte(QMI8658_REG_WHO_AM_I, &who_am_i)) {
ESP_LOGE(TAG, "Failed to read chip ID - check wiring / address");
this->mark_failed();
return;
}
if (who_am_i != QMI8658_WHO_AM_I_VALUE) {
ESP_LOGE(TAG, "Wrong chip ID: 0x%02X (expected 0x%02X)", who_am_i, QMI8658_WHO_AM_I_VALUE);
this->mark_failed();
return;
}
// 2. Soft reset
if (!this->write_byte(QMI8658_REG_RESET, QMI8658_RESET_CMD)) {
this->mark_failed();
return;
}
delay(15); // spec: wait for reset to complete
// 3. Serial interface: enable register address auto-increment
if (!this->write_byte(QMI8658_REG_CTRL1, QMI8658_CTRL1_VALUE)) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL1"));
return;
}
// 4. Configure accelerometer (CTRL2 = range | ODR)
if (!this->write_byte(QMI8658_REG_CTRL2, (uint8_t) (this->accel_range_) | (uint8_t) (this->accel_odr_))) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL2"));
return;
}
// 5. Configure gyroscope (CTRL3 = range | ODR)
if (!this->write_byte(QMI8658_REG_CTRL3, (uint8_t) (this->gyro_range_) | (uint8_t) (this->gyro_odr_))) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL3"));
return;
}
// 6. Disable the built-in low-pass filters (leave raw data to the motion pipeline)
if (!this->write_byte(QMI8658_REG_CTRL5, 0x00)) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL5"));
this->mark_failed();
return;
}
// 7. Enable accelerometer and gyroscope
if (!this->write_byte(QMI8658_REG_CTRL7, QMI8658_CTRL7_ACC_EN | QMI8658_CTRL7_GYR_EN)) {
this->mark_failed(LOG_STR("Failed to write REG_CTRL7"));
return;
}
ESP_LOGCONFIG(TAG, "QMI8658 initialised successfully");
}
void QMI8658Component::dump_config() {
ESP_LOGCONFIG(TAG, "QMI8658 IMU:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, " Communication failed!");
return;
}
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
static constexpr const char *const GYRO_RANGE_STRS[] = {"±16°/s", "±32°/s", "±64°/s", "±128°/s",
"±256°/s", "±512°/s", "±1024°/s", "±2048°/s"};
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[this->accel_range_ >> 4]);
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[this->gyro_range_ >> 4]);
MotionComponent::dump_config();
}
bool QMI8658Component::update_data(motion::MotionData &data) {
if (this->is_failed())
return false;
// Read temperature + accel + gyro in one contiguous block starting at TEMP_L.
uint8_t raw_data[REG_READ_LEN];
if (!this->read_bytes(QMI8658_REG_TEMP_L, raw_data, REG_READ_LEN)) {
ESP_LOGW(TAG, "Failed to read IMU data");
return false;
}
// Data is little-endian (low byte first).
float scale = ACCEL_SCALE[this->accel_range_ >> 4];
int16_t raw_x = encode_uint16(raw_data[ACC_OFFS + 1], raw_data[ACC_OFFS + 0]);
int16_t raw_y = encode_uint16(raw_data[ACC_OFFS + 3], raw_data[ACC_OFFS + 2]);
int16_t raw_z = encode_uint16(raw_data[ACC_OFFS + 5], raw_data[ACC_OFFS + 4]);
ESP_LOGV(TAG, "Read raw accel data: %d, %d, %d", raw_x, raw_y, raw_z);
data.acceleration[motion::X_AXIS] = raw_x * scale;
data.acceleration[motion::Y_AXIS] = raw_y * scale;
data.acceleration[motion::Z_AXIS] = raw_z * scale;
scale = GYRO_SCALE[this->gyro_range_ >> 4];
raw_x = encode_uint16(raw_data[GYR_OFFS + 1], raw_data[GYR_OFFS + 0]);
raw_y = encode_uint16(raw_data[GYR_OFFS + 3], raw_data[GYR_OFFS + 2]);
raw_z = encode_uint16(raw_data[GYR_OFFS + 5], raw_data[GYR_OFFS + 4]);
ESP_LOGV(TAG, "Read raw gyro data: %d, %d, %d", raw_x, raw_y, raw_z);
data.angular_rate[motion::X_AXIS] = raw_x * scale;
data.angular_rate[motion::Y_AXIS] = raw_y * scale;
data.angular_rate[motion::Z_AXIS] = raw_z * scale;
if (this->temperature_callback_.empty())
return true;
// Temperature: signed 16-bit, °C = raw / 256
int16_t raw_t = (int16_t) ((raw_data[TEMP_OFFS + 1] << 8) | raw_data[TEMP_OFFS + 0]);
this->temperature_callback_.call(raw_t / 256.0f);
return true;
}
} // namespace esphome::qmi8658
+112
View File
@@ -0,0 +1,112 @@
#pragma once
#include "esphome/components/motion/motion_component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome::qmi8658 {
// Register map
static constexpr uint8_t QMI8658_REG_WHO_AM_I = 0x00;
static constexpr uint8_t QMI8658_REG_REVISION = 0x01;
static constexpr uint8_t QMI8658_REG_CTRL1 = 0x02; // serial interface / auto-increment
static constexpr uint8_t QMI8658_REG_CTRL2 = 0x03; // accelerometer ODR / range
static constexpr uint8_t QMI8658_REG_CTRL3 = 0x04; // gyroscope ODR / range
static constexpr uint8_t QMI8658_REG_CTRL5 = 0x06; // low-pass filter
static constexpr uint8_t QMI8658_REG_CTRL7 = 0x08; // sensor enable
static constexpr uint8_t QMI8658_REG_STATUS0 = 0x2E;
static constexpr uint8_t QMI8658_REG_TEMP_BASE = 0x33; // start of the data block
static constexpr uint8_t QMI8658_REG_TEMP_L = 0x33; // Low byte of temperature
static constexpr uint8_t QMI8658_REG_AX_L = 0x35;
static constexpr uint8_t QMI8658_REG_GX_L = 0x3B;
static constexpr uint8_t QMI8658_REG_RESET = 0x60;
// One contiguous read covers temperature (2) + accel (6) + gyro (6) starting at TEMP_L.
static constexpr uint8_t REG_READ_LEN = QMI8658_REG_GX_L + 6 - QMI8658_REG_TEMP_BASE; // 0x41 - 0x33 = 14
static constexpr uint8_t TEMP_OFFS = QMI8658_REG_TEMP_L - QMI8658_REG_TEMP_BASE; // 0
static constexpr uint8_t ACC_OFFS = QMI8658_REG_AX_L - QMI8658_REG_TEMP_BASE; // 2
static constexpr uint8_t GYR_OFFS = QMI8658_REG_GX_L - QMI8658_REG_TEMP_BASE; // 8
static constexpr uint8_t QMI8658_WHO_AM_I_VALUE = 0x05;
static constexpr uint8_t QMI8658_RESET_CMD = 0xB0;
// CTRL1: bit6 ADDR_AI (register address auto-increment); little-endian, 4-wire SPI
static constexpr uint8_t QMI8658_CTRL1_VALUE = 0x40;
// CTRL7: aEN (bit0) | gEN (bit1)
static constexpr uint8_t QMI8658_CTRL7_ACC_EN = 0x01;
static constexpr uint8_t QMI8658_CTRL7_GYR_EN = 0x02;
// Accelerometer range options (CTRL2 bits 6:4)
enum QMI8658AccelRange : uint8_t {
QMI8658_ACCEL_RANGE_2G = 0x00,
QMI8658_ACCEL_RANGE_4G = 0x10,
QMI8658_ACCEL_RANGE_8G = 0x20,
QMI8658_ACCEL_RANGE_16G = 0x30,
};
// Accelerometer ODR options (CTRL2 bits 3:0)
enum QMI8658AccelODR : uint8_t {
QMI8658_ACCEL_ODR_8000 = 0x00,
QMI8658_ACCEL_ODR_4000 = 0x01,
QMI8658_ACCEL_ODR_2000 = 0x02,
QMI8658_ACCEL_ODR_1000 = 0x03,
QMI8658_ACCEL_ODR_500 = 0x04,
QMI8658_ACCEL_ODR_250 = 0x05,
QMI8658_ACCEL_ODR_125 = 0x06,
QMI8658_ACCEL_ODR_62_5 = 0x07,
QMI8658_ACCEL_ODR_31_25 = 0x08,
};
// Gyroscope range options (CTRL3 bits 6:4)
enum QMI8658GyroRange : uint8_t {
QMI8658_GYRO_RANGE_16 = 0x00,
QMI8658_GYRO_RANGE_32 = 0x10,
QMI8658_GYRO_RANGE_64 = 0x20,
QMI8658_GYRO_RANGE_128 = 0x30,
QMI8658_GYRO_RANGE_256 = 0x40,
QMI8658_GYRO_RANGE_512 = 0x50,
QMI8658_GYRO_RANGE_1024 = 0x60,
QMI8658_GYRO_RANGE_2048 = 0x70,
};
// Gyroscope ODR options (CTRL3 bits 3:0)
enum QMI8658GyroODR : uint8_t {
QMI8658_GYRO_ODR_8000 = 0x00,
QMI8658_GYRO_ODR_4000 = 0x01,
QMI8658_GYRO_ODR_2000 = 0x02,
QMI8658_GYRO_ODR_1000 = 0x03,
QMI8658_GYRO_ODR_500 = 0x04,
QMI8658_GYRO_ODR_250 = 0x05,
QMI8658_GYRO_ODR_125 = 0x06,
QMI8658_GYRO_ODR_62_5 = 0x07,
QMI8658_GYRO_ODR_31_25 = 0x08,
};
// Main component class
class QMI8658Component : public motion::MotionComponent, public i2c::I2CDevice {
public:
// Lifecycle
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
// Configuration setters
void set_accel_range(QMI8658AccelRange r) { this->accel_range_ = r; }
void set_accel_odr(QMI8658AccelODR o) { this->accel_odr_ = o; }
void set_gyro_range(QMI8658GyroRange r) { this->gyro_range_ = r; }
void set_gyro_odr(QMI8658GyroODR o) { this->gyro_odr_ = o; }
template<typename F> void add_temperature_listener(F &&cb) { this->temperature_callback_.add(std::forward<F>(cb)); }
protected:
bool update_data(motion::MotionData &data) override;
// Config
QMI8658AccelRange accel_range_{QMI8658_ACCEL_RANGE_4G};
QMI8658AccelODR accel_odr_{QMI8658_ACCEL_ODR_1000};
QMI8658GyroRange gyro_range_{QMI8658_GYRO_RANGE_2048};
QMI8658GyroODR gyro_odr_{QMI8658_GYRO_ODR_1000};
LazyCallbackManager<void(float)> temperature_callback_{};
};
} // namespace esphome::qmi8658
+39
View File
@@ -0,0 +1,39 @@
# YAML config keys
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_TEMPERATURE,
CONF_TYPE,
DEVICE_CLASS_TEMPERATURE,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
from esphome.cpp_generator import MockObj
from . import CONF_QMI8658_ID, QMI8658Component
CONFIG_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
device_class=DEVICE_CLASS_TEMPERATURE,
).extend(
{
cv.Optional(CONF_TYPE): cv.one_of(CONF_TEMPERATURE),
cv.GenerateID(CONF_QMI8658_ID): cv.use_id(QMI8658Component),
}
)
async def to_code(config):
var = await sensor.new_sensor(config)
parent = await cg.get_variable(config[CONF_QMI8658_ID])
data = MockObj("data")
value_lambda = await cg.process_lambda(
var.publish_state(data),
[(cg.float_, str(data))],
)
cg.add(parent.add_temperature_listener(value_lambda))
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@
namespace esphome::sdm_meter {
class SDMMeter final : public PollingComponent, public modbus::ModbusDevice {
class SDMMeter final : public PollingComponent, public modbus::ModbusClientDevice {
public:
void set_voltage_sensor(uint8_t phase, sensor::Sensor *voltage_sensor) {
this->phases_[phase].setup = true;
+12 -2
View File
@@ -41,12 +41,15 @@ from esphome.const import (
UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT,
)
from esphome.types import ConfigType
AUTO_LOAD = ["modbus"]
CODEOWNERS = ["@polyfaces", "@jesserockz"]
sdm_meter_ns = cg.esphome_ns.namespace("sdm_meter")
SDMMeter = sdm_meter_ns.class_("SDMMeter", cg.PollingComponent, modbus.ModbusDevice)
SDMMeter = sdm_meter_ns.class_(
"SDMMeter", cg.PollingComponent, modbus.ModbusClientDevice
)
PHASE_SENSORS = {
CONF_VOLTAGE: sensor.sensor_schema(
@@ -145,10 +148,17 @@ CONFIG_SCHEMA = (
)
def _final_validate(config: ConfigType) -> ConfigType:
return modbus.final_validate_modbus_device("sdm_meter", role="client")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await modbus.register_modbus_device(var, config)
await modbus.register_modbus_client_device(var, config)
if CONF_TOTAL_POWER in config:
sens = await sensor.new_sensor(config[CONF_TOTAL_POWER])
+1 -1
View File
@@ -15,7 +15,7 @@ namespace esphome::selec_meter {
public: \
void set_##name##_sensor(sensor::Sensor *(name)) { this->name##_sensor_ = name; }
class SelecMeter final : public PollingComponent, public modbus::ModbusDevice {
class SelecMeter final : public PollingComponent, public modbus::ModbusClientDevice {
public:
SELEC_METER_SENSOR(total_active_energy)
SELEC_METER_SENSOR(import_active_energy)
+10 -2
View File
@@ -32,6 +32,7 @@ from esphome.const import (
UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT,
)
from esphome.types import ConfigType
AUTO_LOAD = ["modbus"]
CODEOWNERS = ["@sourabhjaiswal"]
@@ -49,7 +50,7 @@ UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh"
selec_meter_ns = cg.esphome_ns.namespace("selec_meter")
SelecMeter = selec_meter_ns.class_(
"SelecMeter", cg.PollingComponent, modbus.ModbusDevice
"SelecMeter", cg.PollingComponent, modbus.ModbusClientDevice
)
SENSORS = {
@@ -163,10 +164,17 @@ CONFIG_SCHEMA = (
)
def _final_validate(config: ConfigType) -> ConfigType:
return modbus.final_validate_modbus_device("selec_meter", role="client")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await modbus.register_modbus_device(var, config)
await modbus.register_modbus_client_device(var, config)
for name in SENSORS:
if name in config:
sens = await sensor.new_sensor(config[name])
+5 -5
View File
@@ -266,7 +266,7 @@ class WeikaiComponent : public Component {
///////////////////////////////////////////////////////////////////////////////
/// @brief Helper class to expose a WeiKai family IO pin as an internal GPIO pin.
///////////////////////////////////////////////////////////////////////////////
class WeikaiGPIOPin final : public GPIOPin {
class WeikaiGPIOPin : public GPIOPin {
public:
void set_parent(WeikaiComponent *parent) { this->parent_ = parent; }
void set_pin(uint8_t pin) { this->pin_ = pin; }
@@ -293,7 +293,7 @@ class WeikaiGPIOPin final : public GPIOPin {
/// uart::UARTComponent virtual class. This class is common to the different members of the Weikai
/// components family and therefore avoid code duplication.
///////////////////////////////////////////////////////////////////////////////////////////////////
class WeikaiChannel final : public uart::UARTComponent {
class WeikaiChannel : public uart::UARTComponent {
public:
/// @brief We belongs to this WeikaiComponent
/// @param parent pointer to the component we belongs to
@@ -315,10 +315,10 @@ class WeikaiChannel final : public uart::UARTComponent {
const char *get_channel_name() { return this->name_.c_str(); }
/// @brief Setup the channel
void setup_channel();
void virtual setup_channel();
/// @brief dump channel information
void dump_channel();
void virtual dump_channel();
/// @brief Factory method to create a WeikaiRegister proxy object
/// @param reg address of the register
@@ -414,7 +414,7 @@ class WeikaiChannel final : public uart::UARTComponent {
/// @brief check if channel is alive
/// @return true if OK
bool check_channel_down();
bool virtual check_channel_down();
#ifdef TEST_COMPONENT
/// @ingroup test_
+3 -3
View File
@@ -147,9 +147,9 @@ def _setup_core(work_dir: Path, settings: _Settings) -> None:
from esphome.core import CORE
CORE.name = TIDY_PROJECT_NAME
# config_path's parent is the data dir root: the IDF install lives at
# ``<parent>/.esphome/idf`` -- keep it beside (not inside) the per-run
# project dir so clearing the project doesn't force an IDF re-download.
# config_path's parent is the data dir root for per-run artifacts (idedata,
# converted pio_components). The IDF install is in the global cache dir,
# independent of this path.
CORE.config_path = work_dir.parent / "tidy.yaml"
CORE.build_path = work_dir
esp32 = CORE.data.setdefault(KEY_ESP32, {})
+22 -13
View File
@@ -9,6 +9,8 @@ import re
import shutil
import tempfile
import platformdirs
from esphome.config_validation import Version
from esphome.core import CORE
from esphome.framework_helpers import (
@@ -80,10 +82,18 @@ def _get_idf_tools_path() -> Path:
Returns:
Path object pointing to the ESP-IDF tools directory
"""
if "ESPHOME_ESP_IDF_PREFIX" in os.environ:
path = Path(get_str_env("ESPHOME_ESP_IDF_PREFIX", None)).expanduser()
# Treat an empty/whitespace ESPHOME_ESP_IDF_PREFIX as unset: Path("")
# resolves to the CWD, which would install into (and let clean-all delete)
# the working directory by accident.
if prefix := get_str_env("ESPHOME_ESP_IDF_PREFIX", "").strip():
path = Path(prefix).expanduser()
else:
path = CORE.data_dir / "idf"
# Machine-global so all projects share the multi-GB install instead of
# a per-config-directory copy. The user cache dir (not ~/.esphome)
# avoids colliding with data_dir when configs live in the home dir.
# appauthor=False drops the redundant <author>\ segment on Windows
# (which otherwise repeats "esphome\esphome\") to keep the path short.
path = Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / "idf"
# Resolve so an unnormalized config path (e.g. compiling ``../config/x.yaml``)
# doesn't leave ``..`` segments in the IDF_TOOLS_PATH handed to idf.py, which
# otherwise warns that the venv interpreter path doesn't match the install.
@@ -145,10 +155,11 @@ def _check_windows_path_length() -> None:
" fatal error: bits/c++config.h: No such file or directory\n"
" cannot execute 'as': CreateProcess: No such file or directory\n"
"To fix, either:\n"
" - Enable Windows long path support: set\n"
" HKLM\\SYSTEM\\CurrentControlSet\\Control\\FileSystem\\LongPathsEnabled\n"
" to 1 and reboot, or\n"
" - Move your ESPHome project to a shorter path\n"
" - Enable Windows long path support, then reboot. In an elevated\n"
" PowerShell run:\n"
" Set-ItemProperty 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\FileSystem' LongPathsEnabled 1\n"
" Details: https://learn.microsoft.com/windows/win32/fileio/maximum-file-path-limitation\n"
" - Or set ESPHOME_ESP_IDF_PREFIX to a shorter path (e.g. C:\\ESPHome\\idf)\n"
"Then delete the ESP-IDF tools directory above so the toolchain "
"reinstalls cleanly.",
tools_path,
@@ -553,7 +564,7 @@ def _check_esphome_idf_framework_install(
# Logged every invocation (not just on install) so the user can verify the
# override. A changed URL needs ``esphome clean-all`` to force a re-download
# (``esphome clean`` only wipes the build dir, not the extracted framework
# under <data_dir>/idf/frameworks/<version>).
# under the global install dir's ``frameworks/<version>``).
if source_url:
_LOGGER.info("Using framework source override: %s", source_url)
@@ -822,11 +833,9 @@ def _ccache_env() -> dict[str, str]:
Enabled by default whenever the ``ccache`` binary is on PATH; set
``IDF_CCACHE_ENABLE=0`` in the environment to opt out. The cache lives under
the IDF tools path. How widely it is shared depends on where that resolves:
across projects (and surviving ``clean-all``) when it is a common location
(``ESPHOME_ESP_IDF_PREFIX`` or the add-on ``/data``), but per-project under
``.esphome/idf`` for a default pip install, where ``clean-all`` clears it
along with the framework.
the IDF tools path (the machine-global cache dir, or
``ESPHOME_ESP_IDF_PREFIX``), so it is shared across all projects and removed
by ``esphome clean-all`` along with the framework.
Depend mode keeps cache-miss overhead low (hashes the compiler's depfiles
instead of preprocessing). ``CCACHE_BASEDIR`` rewrites the per-build
+9
View File
@@ -653,6 +653,15 @@ def clean_all(configuration: list[str]):
elif item.is_dir() and item.name != "storage":
rmtree(item)
# The native ESP-IDF install lives in a machine-global cache dir, outside
# any .esphome data dir, so the per-config loop above won't reach it.
from esphome.espidf.framework import _get_idf_tools_path
idf_install_path = _get_idf_tools_path()
if idf_install_path.is_dir():
_LOGGER.info("Deleting %s", idf_install_path)
rmtree(idf_install_path)
# Clean PlatformIO project files
try:
from platformio.project.config import ProjectConfig
+2 -2
View File
@@ -224,7 +224,7 @@ build_unflags =
; This are common settings for the LibreTiny (all variants) using Arduino.
[common:libretiny-arduino]
extends = common:arduino
platform = https://github.com/libretiny-eu/libretiny.git#v1.12.1
platform = https://github.com/libretiny-eu/libretiny.git#v1.13.0
framework = arduino
lib_compat_mode = soft
lib_deps =
@@ -525,7 +525,7 @@ build_unflags =
[env:ln882h-arduino]
extends = common:libretiny-arduino
board = generic-ln882hki
board = generic-ln882h
build_flags =
${common:libretiny-arduino.build_flags}
${flags:runtime.build_flags}
+1
View File
@@ -23,6 +23,7 @@ bleak==2.1.1
smpclient==6.0.0
requests==2.34.2
py7zr==1.1.3
platformdirs==4.10.0 # native esp-idf toolchain global cache dir
# esp-idf >= 5.0 requires this
pyparsing >= 3.3.2
+78 -21
View File
@@ -238,6 +238,72 @@ class _ConflictWalk:
rejects: set[str]
@cache
def _get_test_config_components(component: str, platform: str) -> frozenset[str]:
"""Return the components referenced by a component's test config for a platform.
Loads ``tests/components/<component>/test.<platform>.yaml`` and extracts the
top-level component keys (and list ``platform:`` values). This lets the
conflict splitter see components that are only pulled in via a test config
(e.g. nRF52 ``network`` tests that also enable ``openthread``), which a
purely static AUTO_LOAD/CONFLICTS_WITH parse cannot discover -- notably for
components like ``api`` whose ``AUTO_LOAD`` is a callable.
Failures (missing file, parse error) are treated as empty so the splitter
never crashes on a malformed or absent test config.
"""
from esphome import yaml_util
test_file = (
Path(root_path) / "tests" / "components" / component / f"test.{platform}.yaml"
)
if not test_file.exists():
return frozenset()
try:
config = yaml_util.load_yaml(test_file)
except Exception: # noqa: BLE001 - never let a bad test config crash grouping
# Matches analyze_component_buses, which loads these same files and
# silently tolerates parse failures; surfacing it only here would be
# inconsistent and noisy.
return frozenset()
if not isinstance(config, dict):
return frozenset()
return frozenset(_extract_components_from_yaml(config))
@cache
def _conflict_walk(comp: str, platform: str) -> _ConflictWalk:
"""Build the platform-aware conflict walk for a single component.
Seeds the walk with the component itself plus any components pulled in via
its ``test.<platform>.yaml`` config, then folds in each seed's static
AUTO_LOAD closure and CONFLICTS_WITH declarations. Cached per
``(component, platform)`` since the test-config seeds are platform-specific.
"""
seeds = {comp} | set(_get_test_config_components(comp, platform))
walk = _ConflictWalk(loaded=set(seeds), rejects=set())
stack = list(seeds)
while stack:
metadata = parse_component_metadata(stack.pop())
walk.rejects |= metadata.conflicts_with
new = metadata.auto_load - walk.loaded
walk.loaded |= new
stack.extend(new)
return walk
def components_conflict(a: str, b: str, platform: str) -> bool:
"""Return True if components ``a`` and ``b`` cannot share a build on ``platform``.
Uses the same platform-aware conflict walk as :func:`split_conflicting_groups`
so callers (e.g. the no-bus redistribution in ``test_build_components.py``)
agree with how groups were originally split. The conflict relation is
symmetric even when only one side declares CONFLICTS_WITH.
"""
wa, wb = _conflict_walk(a, platform), _conflict_walk(b, platform)
return not wa.rejects.isdisjoint(wb.loaded) or not wb.rejects.isdisjoint(wa.loaded)
def split_conflicting_groups(
grouped_components: dict[tuple[str, str], list[str]],
) -> dict[tuple[str, str], list[str]]:
@@ -250,33 +316,24 @@ def split_conflicting_groups(
conflict relation is treated as symmetric even when only one side
declares it (e.g. ethernet rejects wifi but wifi does not declare the
reverse).
The walk is platform-aware: in addition to the static AUTO_LOAD closure,
each ``(component, platform)`` walk is seeded with the components found in
that component's ``test.<platform>.yaml`` config. This catches conflicts
that only exist on a given platform and are expressed through the test
config rather than static metadata -- e.g. on nRF52 the ``network``/``api``
test configs also enable ``openthread``, which ``zigbee`` declares a
conflict with, so ``api`` and ``zigbee`` end up split there. On ESP32 those
test configs have no ``openthread``, so the components still group together.
"""
batch = {c for comps in grouped_components.values() for c in comps}
walks: dict[str, _ConflictWalk] = {}
for comp in batch:
walk = _ConflictWalk(loaded={comp}, rejects=set())
stack = [comp]
while stack:
metadata = parse_component_metadata(stack.pop())
walk.rejects |= metadata.conflicts_with
new = metadata.auto_load - walk.loaded
walk.loaded |= new
stack.extend(new)
walks[comp] = walk
def conflicts(a: str, b: str) -> bool:
wa, wb = walks[a], walks[b]
return not wa.rejects.isdisjoint(wb.loaded) or not wb.rejects.isdisjoint(
wa.loaded
)
result: dict[tuple[str, str], list[str]] = {}
for (platform, signature), components in grouped_components.items():
buckets: list[list[str]] = []
for comp in components:
for bucket in buckets:
if not any(conflicts(comp, other) for other in bucket):
if not any(
components_conflict(comp, other, platform) for other in bucket
):
bucket.append(comp)
break
else:
+1 -1
View File
@@ -1,5 +1,5 @@
{
"target_module": "esphome.__main__",
"margin_pct": 15,
"margin_pct": 20,
"cumulative_us": 91000
}
+13 -2
View File
@@ -4,7 +4,12 @@
set -e
cd "$(dirname "$0")/.."
if [ ! -n "$VIRTUAL_ENV" ]; then
if [ -n "$VIRTUAL_ENV" ]; then
# A virtual environment is already active (e.g. the devcontainer's pre-provisioned
# esphome-venv). Install into it rather than creating a ./venv in the workspace.
created_venv=false
else
created_venv=true
if [ -x "$(command -v uv)" ]; then
uv venv --seed venv
else
@@ -26,4 +31,10 @@ mkdir -p .temp
echo
echo
echo "Virtual environment created. Run 'source venv/bin/activate' to use it."
if [ "$created_venv" = true ]; then
echo "Virtual environment created at ./venv. Run 'source venv/bin/activate' to use it."
else
echo "Dependencies installed into the active virtual environment:"
echo " $VIRTUAL_ENV"
echo "It is already active in this shell, so no 'source venv/bin/activate' is needed."
fi
+30 -8
View File
@@ -40,6 +40,7 @@ from script.analyze_component_buses import (
uses_local_file_references,
)
from script.helpers import (
components_conflict,
get_component_test_files,
is_validate_only_file,
parse_test_filename,
@@ -788,14 +789,35 @@ def run_grouped_component_tests(
if plat == platform and sig != NO_BUSES_SIGNATURE
]
if platform_groups:
# Distribute no_buses components round-robin across existing groups
for i, comp in enumerate(no_buses_comps):
sig, _ = platform_groups[i % len(platform_groups)]
grouped_components[(platform, sig)].append(comp)
else:
# No other groups for this platform - keep no_buses components together
grouped_components[(platform, NO_BUSES_SIGNATURE)] = no_buses_comps
# Distribute no_buses components round-robin across existing groups,
# but never place a component into a group it conflicts with. Conflict
# splitting (split_conflicting_groups) may have created sibling groups
# like "no_buses__conflict1" precisely to keep incompatible components
# apart (e.g. on nRF52, network pulls in openthread which zigbee
# conflicts with); redistribution must not silently undo that split.
leftover: list[str] = []
for i, comp in enumerate(no_buses_comps):
placed = False
# Try groups starting at the round-robin offset to keep the spread.
for offset in range(len(platform_groups)):
sig, comps = platform_groups[(i + offset) % len(platform_groups)]
if any(components_conflict(comp, other, platform) for other in comps):
continue
# comps is the same list object stored in grouped_components, so
# this also extends the group in grouped_components.
comps.append(comp)
placed = True
break
if not placed:
leftover.append(comp)
if leftover:
# Components that conflict with every existing group stay together in
# their own no_buses group (they were grouped before, so they don't
# conflict with each other).
grouped_components.setdefault((platform, NO_BUSES_SIGNATURE), []).extend(
leftover
)
groups_to_test = []
individual_tests = set() # Use set to avoid duplicates
+1 -1
View File
@@ -377,6 +377,6 @@ def test_lvgl_generation(
"mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0, 0, 0, 0, true>();"
in main_cpp
)
assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp
assert "set_init_sequence({177, 3, 1, 44, 45, 178" in main_cpp
assert "show_test_card();" not in main_cpp
assert "set_auto_clear(false);" in main_cpp
@@ -1,4 +1,7 @@
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
api:
+16
View File
@@ -0,0 +1,16 @@
display:
- id: cst9220_display
platform: ili9xxx
model: ili9342
cs_pin: ${cs_pin}
dc_pin: ${dc_pin}
reset_pin: ${disp_reset_pin}
invert_colors: false
touchscreen:
- id: ts_cst9220
i2c_id: i2c_bus
platform: cst9220
display: cst9220_display
interrupt_pin: ${interrupt_pin}
reset_pin: ${reset_pin}
@@ -0,0 +1,12 @@
substitutions:
cs_pin: GPIO4
dc_pin: GPIO5
disp_reset_pin: GPIO12
interrupt_pin: GPIO15
reset_pin: GPIO25
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
<<: !include common.yaml
@@ -1,4 +1,7 @@
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
mdns:
@@ -0,0 +1,2 @@
packages:
common: !include common.yaml
@@ -1 +1,5 @@
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
@@ -1 +1,5 @@
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
@@ -1 +1,5 @@
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
+26
View File
@@ -0,0 +1,26 @@
display:
- platform: pixoo
id: pixoo_display
model: 64x64
cs_pin: GPIO5
data_rate: 10MHz
update_interval: 1s
lambda: |-
it.fill(Color(0, 0, 0));
it.filled_rectangle(0, 0, 16, 16, Color(255, 0, 0));
it.line(0, 0, 63, 63, Color(0, 255, 0));
- platform: pixoo
id: pixoo_display_pages
model: 64x64
cs_pin: GPIO21
rotation: 90
pages:
- id: pixoo_page
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height(), Color(0, 0, 255));
light:
- platform: pixoo
pixoo_id: pixoo_display
name: Pixoo Brightness
@@ -0,0 +1,4 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
<<: !include common.yaml
+70
View File
@@ -0,0 +1,70 @@
sensor:
- platform: qmi8658
name: "QMI8658 Temperature"
- platform: motion
type: acceleration_x
name: "Accel X"
accuracy_decimals: 4
filters:
- sliding_window_moving_average:
window_size: 4
send_every: 1
- platform: motion
type: acceleration_y
name: "Accel Y"
accuracy_decimals: 4
- platform: motion
type: acceleration_z
name: "Accel Z"
accuracy_decimals: 4
# Gyroscope axes (unit: °/s)
- platform: motion
type: gyroscope_x
name: "Gyro X"
- platform: motion
type: gyroscope_y
name: "Gyro Y"
- platform: motion
type: gyroscope_z
name: "Gyro Z"
- platform: motion
type: angular_rate_x
name: "Angular Rate X"
- platform: motion
type: angular_rate_y
name: "Angular Rate Y"
- platform: motion
type: angular_rate_z
name: "Angular Rate Z"
- platform: motion
type: pitch
name: "Pitch"
- platform: motion
type: roll
name: "Roll"
motion:
- platform: qmi8658
i2c_id: i2c_bus
# Accelerometer full-scale range: 2G | 4G | 8G | 16G
accelerometer_range: 4G
# Accelerometer output data rate: 31_25HZ | 62_5HZ | 125HZ | 250HZ |
# 500HZ | 1000HZ | 2000HZ | 4000HZ | 8000HZ
accelerometer_odr: 1000HZ
# Gyroscope full-scale range: 16DPS | 32DPS | 64DPS | 128DPS |
# 256DPS | 512DPS | 1024DPS | 2048DPS
gyroscope_range: 2048DPS
# Gyroscope output data rate: 31_25HZ | 62_5HZ | 125HZ | 250HZ |
# 500HZ | 1000HZ | 2000HZ | 4000HZ | 8000HZ
gyroscope_odr: 1000HZ
axis_map:
x: y
y: x
z: -z
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
<<: !include common.yaml
+45
View File
@@ -35,6 +35,8 @@ def clear_helpers_cache() -> None:
helpers._get_github_event_data.cache_clear()
helpers._get_changed_files_github_actions.cache_clear()
helpers.get_components_per_integration_fixture.cache_clear()
helpers._get_test_config_components.cache_clear()
helpers._conflict_walk.cache_clear()
@pytest.mark.parametrize(
@@ -1504,6 +1506,8 @@ def fake_components(tmp_path: Path) -> Path:
write("callable_auto", "def AUTO_LOAD():\n return ['beta']\n")
write("broken", "this is not valid python !!!")
helpers.parse_component_metadata.cache_clear()
helpers._get_test_config_components.cache_clear()
helpers._conflict_walk.cache_clear()
return tmp_path
@@ -1624,6 +1628,47 @@ def test_split_conflicting_groups_preserves_original_signature_for_first_bucket(
assert signature.startswith("i2c__conflict")
def test_split_conflicting_groups_seeds_from_test_config(
fake_components: Path, monkeypatch: MonkeyPatch
) -> None:
"""A conflict reachable only via a component's test config splits the group.
``host_user`` declares no static conflict with ``beta``, but its
``test.<platform>.yaml`` pulls in ``beta_variant`` (which AUTO_LOADs
``beta``). On that platform the group must split; on another platform
(no such test config) it must stay together.
"""
monkeypatch.setattr(helpers, "root_path", str(fake_components))
# host_user has no static metadata, but its esp32 test config references
# beta_variant -> AUTO_LOAD beta, which conflicts with alpha.
tests_dir = fake_components / "tests" / "components" / "host_user"
tests_dir.mkdir(parents=True)
(tests_dir / "test.esp32.yaml").write_text("beta_variant:\n")
(fake_components / "esphome" / "components" / "host_user").mkdir()
(
fake_components / "esphome" / "components" / "host_user" / "__init__.py"
).write_text("")
helpers.parse_component_metadata.cache_clear()
helpers._get_test_config_components.cache_clear()
helpers._conflict_walk.cache_clear()
# On esp32, host_user pulls in beta (via its test config) -> conflicts with alpha.
result = helpers.split_conflicting_groups(
{("esp32", "no_buses"): ["alpha", "host_user"]}
)
buckets = list(result.values())
for bucket in buckets:
assert not ({"alpha", "host_user"} <= set(bucket))
# On a platform without that test config, they stay grouped together.
result_other = helpers.split_conflicting_groups(
{("rp2040", "no_buses"): ["alpha", "host_user"]}
)
assert result_other == {("rp2040", "no_buses"): ["alpha", "host_user"]}
# ---------------------------------------------------------------------------
# get_component_test_files / is_validate_only_file
# ---------------------------------------------------------------------------
@@ -0,0 +1,18 @@
esphome:
name: componenttestesp32c61idf
friendly_name: $component_name
esp32:
variant: ESP32C61
flash_size: 8MB
framework:
type: esp-idf
logger:
level: VERY_VERBOSE
packages:
component_under_test: !include
file: $component_test_file
vars:
component_test_file: $component_test_file
@@ -3,7 +3,7 @@ esphome:
friendly_name: $component_name
ln882x:
board: generic-ln882hki
board: generic-ln882h
logger:
level: VERY_VERBOSE
+47
View File
@@ -36,6 +36,19 @@ from esphome.espidf.framework import (
from esphome.framework_helpers import _tar_extract_all, get_python_env_executable_path
@pytest.fixture(autouse=True)
def _isolate_idf_install_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Pin the ESP-IDF install root to a tmp dir for every test.
The default location is the OS user cache dir, so without this any test
that builds framework paths or pre-creates the framework dir would touch
the real ``~/.cache/esphome`` on the developer's machine. Tests that need
to exercise the override or default-resolution logic clear/override the env
themselves.
"""
monkeypatch.setenv("ESPHOME_ESP_IDF_PREFIX", str(tmp_path / "idf_install"))
@pytest.mark.parametrize(
("source", "expected"),
[
@@ -791,6 +804,38 @@ def test_get_idf_tools_path_env_override(tmp_path: Path) -> None:
assert _get_idf_tools_path() == Path(override)
@pytest.mark.parametrize("value", ["", " "])
def test_get_idf_tools_path_blank_env_falls_back_to_default(
value: str, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A blank ESPHOME_ESP_IDF_PREFIX is treated as unset, not as CWD.
Path("") would resolve to the working directory, which clean-all could then
delete by accident.
"""
import platformdirs
monkeypatch.setenv("ESPHOME_ESP_IDF_PREFIX", value)
expected = (
Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / "idf"
).resolve()
assert _get_idf_tools_path() == expected
def test_get_idf_tools_path_default_uses_user_cache(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Without the env override the install root is the machine-global OS user
cache dir, not the per-config ``<data_dir>/idf``."""
import platformdirs
monkeypatch.delenv("ESPHOME_ESP_IDF_PREFIX", raising=False)
expected = (
Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / "idf"
).resolve()
assert _get_idf_tools_path() == expected
def test_write_idf_version_txt_warns_on_write_error(tmp_path: Path) -> None:
with patch("pathlib.Path.write_text", side_effect=OSError("denied")):
# write failure is caught and warned, not raised
@@ -908,3 +953,5 @@ def test_check_windows_path_length_long_path_warns(
message = caplog.records[0].getMessage()
assert _LONG_IDF_PATH in message
assert "long path support" in message
# The install is global now; the remedy is the prefix env, not moving the project.
assert "ESPHOME_ESP_IDF_PREFIX" in message
+35 -3
View File
@@ -67,15 +67,23 @@ def _isolate_platformio_paths(tmp_path_factory: pytest.TempPathFactory) -> Any:
want to verify the PIO-cleanup branch (e.g. test_clean_all,
test_clean_all_partial_exists) install their own inner patch which
stacks on top of this one and wins for the duration of their block.
Also pin ``ESPHOME_ESP_IDF_PREFIX`` to a nonexistent tmp dir for the
same reason: ``clean_all`` removes the now machine-global ESP-IDF
install, which otherwise defaults to the real ``~/.cache/esphome``.
"""
pio_root = tmp_path_factory.mktemp("isolated_pio") / "nonexistent"
idf_root = tmp_path_factory.mktemp("isolated_idf") / "nonexistent"
mock_cfg = MagicMock()
mock_cfg.get.side_effect = lambda section, option: (
str(pio_root / option) if section == "platformio" else ""
)
with patch(
"platformio.project.config.ProjectConfig.get_instance",
return_value=mock_cfg,
with (
patch(
"platformio.project.config.ProjectConfig.get_instance",
return_value=mock_cfg,
),
patch.dict("os.environ", {"ESPHOME_ESP_IDF_PREFIX": str(idf_root)}),
):
yield
@@ -990,6 +998,30 @@ def test_clean_all_with_yaml_file(
assert str(build_dir) in caplog.text
@patch("esphome.writer.CORE")
def test_clean_all_removes_global_idf_install(
mock_core: MagicMock,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""clean_all removes the machine-global native ESP-IDF install dir."""
idf_install = tmp_path / "idf_install"
(idf_install / "frameworks").mkdir(parents=True)
monkeypatch.setenv("ESPHOME_ESP_IDF_PREFIX", str(idf_install))
config_dir = tmp_path / "config"
config_dir.mkdir()
from esphome.writer import clean_all
with caplog.at_level("INFO"):
clean_all([str(config_dir)])
assert not idf_install.exists()
assert str(idf_install.resolve()) in caplog.text
@patch("esphome.writer.CORE")
def test_clean_all_with_yaml_build_path(
mock_core: MagicMock,