mirror of
https://github.com/esphome/esphome.git
synced 2026-07-01 13:02:50 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58780cd765 | |||
| 1fc73731a8 | |||
| e47feace11 | |||
| 4472d3b61b | |||
| 06c5bcbc66 | |||
| 6c44775bf5 | |||
| b127363fa0 | |||
| 782b58bbeb | |||
| 5de508ad8c | |||
| 054c8ba485 | |||
| 3b2be021b2 | |||
| 848defedd8 | |||
| 4c9ed129cf | |||
| c8b37fb1c8 | |||
| 1b556f5d0c | |||
| 8a3d0aeafb | |||
| 9468ad628c | |||
| 990431aa5b | |||
| b79cbcbde7 | |||
| afb5922f37 | |||
| 3035355c0a | |||
| 43b3aa0712 | |||
| 12b78e7c47 | |||
| fb5d8b5d4c | |||
| 9e72027b64 | |||
| faa5f72500 | |||
| 359c6a7265 | |||
| a031da351d | |||
| 091b6a0ba0 | |||
| cf9d97d5ae | |||
| 3e1a6b4e11 | |||
| 5c7245dfcd |
@@ -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
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,6 @@ esphome:
|
||||
name: docker-test-ln882x-arduino
|
||||
|
||||
ln882x:
|
||||
board: generic-ln882hki
|
||||
board: generic-ln882h
|
||||
|
||||
logger:
|
||||
|
||||
+4
-1
@@ -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="*"
|
||||
|
||||
+1127
-1051
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);
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 = {}
|
||||
@@ -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]))
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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))
|
||||
+1371
-1371
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"target_module": "esphome.__main__",
|
||||
"margin_pct": 15,
|
||||
"margin_pct": 20,
|
||||
"cumulative_us": 91000
|
||||
}
|
||||
|
||||
+13
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user