mirror of
https://github.com/esphome/esphome.git
synced 2026-07-01 04:56:09 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58780cd765 | |||
| 1fc73731a8 | |||
| e47feace11 | |||
| 4472d3b61b | |||
| 06c5bcbc66 | |||
| 6c44775bf5 | |||
| b127363fa0 | |||
| 782b58bbeb | |||
| 5de508ad8c | |||
| 054c8ba485 | |||
| 3b2be021b2 | |||
| 848defedd8 | |||
| 4c9ed129cf | |||
| c8b37fb1c8 | |||
| 1b556f5d0c | |||
| 8a3d0aeafb | |||
| 9468ad628c | |||
| 990431aa5b | |||
| b79cbcbde7 | |||
| afb5922f37 |
@@ -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
|
||||
|
||||
+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.22
|
||||
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
|
||||
|
||||
+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="*"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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