Compare commits

..

1 Commits

Author SHA1 Message Date
J. Nick Koston 614d018e4b [core] Collapse duplicate to_remove_ read in Scheduler::call fast path
Read to_remove_ once at the top of the cleanup block instead of loading
it twice (once via cleanup_() -> to_remove_empty_(), once again for the
MAX_LOGICALLY_DELETED_ITEMS check). GCC cannot CSE across the memw
barriers that std::atomic<uint32_t>::load emits on Xtensa, so both the
fast-path zero check and the max check were generating independent
memw + l32i sequences:

    memw; l32i a8, [to_remove_]; beqz ...   ; to_remove_empty_()
    memw; l32i a8, [to_remove_]; bltui 5    ; to_remove_count_()

Reading the counter once into a register and branching on the result
collapses the common zero-case to a single memw + l32i + beqz. The
non-zero path pays one extra read after cleanup_slow_path_ (which may
have decremented the counter), but that path already takes lock_ so the
extra load is negligible.

Scheduler::call stays at 344 B (unchanged); no flash layout shift, so
adjacent code (feed_wdt_slow_ etc.) stays in the same cache lines and
the measurement can't be confounded by MMIO timing drift.

An earlier version of this change also marked Scheduler::millis_64()
ESPHOME_ALWAYS_INLINE to drop its out-of-line $isra$0 clone; that
grew Scheduler::call by 56 B, shifted feed_wdt_slow_ in flash, and
measurably regressed the wdt bucket on a busier winefridge.yaml
config, so the inlining is dropped. This change keeps only the memw
collapse.
2026-04-24 16:56:02 -05:00
233 changed files with 1680 additions and 6101 deletions
+3 -33
View File
@@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -58,7 +58,7 @@ jobs:
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
pip install -r requirements.txt -r requirements_test.txt pre-commit
pip install -e .
pylint:
@@ -108,34 +108,6 @@ jobs:
script/generate-esp32-boards.py --check
script/generate-rp2040-boards.py --check
import-time:
name: Check import esphome.__main__ time
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.import-time == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Check import time against budget and write waterfall HAR
run: |
. venv/bin/activate
script/check_import_time.py --check --har importtime.har
- name: Upload waterfall HAR
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: import-time-waterfall
path: importtime.har
if-no-files-found: ignore
retention-days: 14
pytest:
name: Run pytest
strategy:
@@ -204,7 +176,6 @@ jobs:
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
python-linters: ${{ steps.determine.outputs.python-linters }}
import-time: ${{ steps.determine.outputs.import-time }}
changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
@@ -248,7 +219,6 @@ jobs:
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
@@ -369,7 +339,7 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0
uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0
with:
run: ${{ steps.build.outputs.binary }}
mode: simulation
-1
View File
@@ -146,6 +146,5 @@ sdkconfig.*
/components
/managed_components
/dependencies.lock
api-docs/
-1
View File
@@ -347,7 +347,6 @@ esphome/components/modbus_controller/select/* @martgras @stegm
esphome/components/modbus_controller/sensor/* @martgras
esphome/components/modbus_controller/switch/* @martgras
esphome/components/modbus_controller/text_sensor/* @martgras
esphome/components/modbus_server/* @exciton
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
esphome/components/mopeka_pro_check/* @spbrogan
esphome/components/mopeka_std_check/* @Fabian-Schmidt
+1 -1
View File
@@ -597,7 +597,7 @@ async def component_resume_action_to_code(
comp = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, comp)
if CONF_UPDATE_INTERVAL in config:
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int)
cg.add(var.set_update_interval(template_))
return var
@@ -13,11 +13,7 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@grahambrown11", "@hwstar"]
@@ -185,7 +181,7 @@ async def setup_alarm_control_panel_core_(var, config):
async def register_alarm_control_panel(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
queue_entity_register("alarm_control_panel", config)
cg.add(cg.App.register_alarm_control_panel(var))
CORE.register_platform_component("alarm_control_panel", var)
await setup_alarm_control_panel_core_(var, config)
+1 -6
View File
@@ -62,12 +62,7 @@ void Animation::set_frame(int frame) {
}
void Animation::update_data_start_() {
uint32_t image_size = this->get_width_stride() * this->height_;
// RGB565 with an alpha channel stores the alpha plane immediately after the RGB
// plane within each frame, so the per-frame stride includes the alpha bytes.
if (this->type_ == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
image_size += static_cast<uint32_t>(this->width_) * this->height_;
}
const uint32_t image_size = this->get_width_stride() * this->height_;
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
}
+1 -12
View File
@@ -1025,13 +1025,6 @@ message CameraImageRequest {
bool stream = 2;
}
// ==================== TEMPERATURE UNIT ====================
enum TemperatureUnit {
TEMPERATURE_UNIT_CELSIUS = 0;
TEMPERATURE_UNIT_FAHRENHEIT = 1;
TEMPERATURE_UNIT_KELVIN = 2;
}
// ==================== CLIMATE ====================
enum ClimateMode {
CLIMATE_MODE_OFF = 0;
@@ -1117,7 +1110,6 @@ message ListEntitiesClimateResponse {
float visual_max_humidity = 25;
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
uint32 feature_flags = 27;
TemperatureUnit temperature_unit = 28;
}
message ClimateStateResponse {
option (id) = 47;
@@ -1211,7 +1203,6 @@ message ListEntitiesWaterHeaterResponse {
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
// Bitmask of WaterHeaterFeature flags
uint32 supported_features = 12;
TemperatureUnit temperature_unit = 13;
}
message WaterHeaterStateResponse {
@@ -1419,8 +1410,6 @@ enum LockState {
LOCK_STATE_JAMMED = 3;
LOCK_STATE_LOCKING = 4;
LOCK_STATE_UNLOCKING = 5;
LOCK_STATE_OPENING = 6;
LOCK_STATE_OPEN = 7;
}
enum LockCommand {
LOCK_UNLOCK = 0;
@@ -1639,7 +1628,7 @@ message BluetoothLEAdvertisementResponse {
message BluetoothLERawAdvertisement {
option (inline_encode) = true;
uint64 address = 1 [(force) = true, (mac_address) = true];
uint64 address = 1 [(force) = true];
sint32 rssi = 2 [(force) = true];
uint32 address_type = 3 [(max_value) = 4];
-6
View File
@@ -110,10 +110,4 @@ extend google.protobuf.FieldOptions {
// length varint calculations and direct byte writes, since the length
// varint is guaranteed to be 1 byte.
optional uint32 max_data_length = 50018;
// mac_address: Field is a 48-bit MAC address stored in a uint64.
// Emits encode_varint_raw_48bit which has a 7-byte fast path that avoids
// the per-byte loop when the upper bits are non-zero (the common case
// for real MAC addresses, since OUIs occupy the top 24 bits).
optional bool mac_address = 50019 [default=false];
}
+2 -6
View File
@@ -1439,7 +1439,6 @@ uint8_t *ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 26, this->device_id);
#endif
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 27, this->feature_flags);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 28, static_cast<uint32_t>(this->temperature_unit));
return pos;
}
uint32_t ListEntitiesClimateResponse::calculate_size() const {
@@ -1489,7 +1488,6 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const {
size += ProtoSize::calc_uint32(2, this->device_id);
#endif
size += ProtoSize::calc_uint32(2, this->feature_flags);
size += this->temperature_unit ? 3 : 0;
return size;
}
uint8_t *ClimateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1647,7 +1645,6 @@ uint8_t *ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer PROTO_
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast<uint32_t>(it), true);
}
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supported_features);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, static_cast<uint32_t>(this->temperature_unit));
return pos;
}
uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const {
@@ -1670,7 +1667,6 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const {
size += this->supported_modes->size() * 2;
}
size += ProtoSize::calc_uint32(1, this->supported_features);
size += this->temperature_unit ? 2 : 0;
return size;
}
uint8_t *WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -2352,7 +2348,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
uint8_t *len_pos = pos;
ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi));
if (sub_msg.address_type) {
@@ -2373,7 +2369,7 @@ BluetoothLERawAdvertisementsResponse::calculate_size() const {
for (uint16_t i = 0; i < this->advertisements_len; i++) {
auto &sub_msg = this->advertisements[i];
size += 2;
size += ProtoSize::calc_uint64_48bit_force(1, sub_msg.address);
size += ProtoSize::calc_uint64_force(1, sub_msg.address);
size += ProtoSize::calc_sint32_force(1, sub_msg.rssi);
size += sub_msg.address_type ? 2 : 0;
size += 2 + sub_msg.data_len;
+2 -11
View File
@@ -92,11 +92,6 @@ enum SupportsResponseType : uint32_t {
SUPPORTS_RESPONSE_STATUS = 100,
};
#endif
enum TemperatureUnit : uint32_t {
TEMPERATURE_UNIT_CELSIUS = 0,
TEMPERATURE_UNIT_FAHRENHEIT = 1,
TEMPERATURE_UNIT_KELVIN = 2,
};
#ifdef USE_CLIMATE
enum ClimateMode : uint32_t {
CLIMATE_MODE_OFF = 0,
@@ -181,8 +176,6 @@ enum LockState : uint32_t {
LOCK_STATE_JAMMED = 3,
LOCK_STATE_LOCKING = 4,
LOCK_STATE_UNLOCKING = 5,
LOCK_STATE_OPENING = 6,
LOCK_STATE_OPEN = 7,
};
enum LockCommand : uint32_t {
LOCK_UNLOCK = 0,
@@ -1379,7 +1372,7 @@ class CameraImageRequest final : public ProtoDecodableMessage {
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 46;
static constexpr uint8_t ESTIMATED_SIZE = 153;
static constexpr uint8_t ESTIMATED_SIZE = 150;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_climate_response"); }
#endif
@@ -1401,7 +1394,6 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
uint32_t feature_flags{0};
enums::TemperatureUnit temperature_unit{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1479,7 +1471,7 @@ class ClimateCommandRequest final : public CommandProtoMessage {
class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 132;
static constexpr uint8_t ESTIMATED_SIZE = 65;
static constexpr uint8_t ESTIMATED_SIZE = 63;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_water_heater_response"); }
#endif
@@ -1488,7 +1480,6 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
float target_temperature_step{0.0f};
const water_heater::WaterHeaterModeMask *supported_modes{};
uint32_t supported_features{0};
enums::TemperatureUnit temperature_unit{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
-18
View File
@@ -297,18 +297,6 @@ template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums::
}
}
#endif
template<> const char *proto_enum_to_string<enums::TemperatureUnit>(enums::TemperatureUnit value) {
switch (value) {
case enums::TEMPERATURE_UNIT_CELSIUS:
return ESPHOME_PSTR("TEMPERATURE_UNIT_CELSIUS");
case enums::TEMPERATURE_UNIT_FAHRENHEIT:
return ESPHOME_PSTR("TEMPERATURE_UNIT_FAHRENHEIT");
case enums::TEMPERATURE_UNIT_KELVIN:
return ESPHOME_PSTR("TEMPERATURE_UNIT_KELVIN");
default:
return ESPHOME_PSTR("UNKNOWN");
}
}
#ifdef USE_CLIMATE
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
switch (value) {
@@ -487,10 +475,6 @@ template<> const char *proto_enum_to_string<enums::LockState>(enums::LockState v
return ESPHOME_PSTR("LOCK_STATE_LOCKING");
case enums::LOCK_STATE_UNLOCKING:
return ESPHOME_PSTR("LOCK_STATE_UNLOCKING");
case enums::LOCK_STATE_OPENING:
return ESPHOME_PSTR("LOCK_STATE_OPENING");
case enums::LOCK_STATE_OPEN:
return ESPHOME_PSTR("LOCK_STATE_OPEN");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -1555,7 +1539,6 @@ const char *ListEntitiesClimateResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
#endif
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit));
return out.c_str();
}
const char *ClimateStateResponse::dump_to(DumpBuffer &out) const {
@@ -1629,7 +1612,6 @@ const char *ListEntitiesWaterHeaterResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast<enums::WaterHeaterMode>(it), 4);
}
dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features);
dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit));
return out.c_str();
}
const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const {
@@ -21,7 +21,6 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
}
#endif
#ifdef USE_API
void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements
switch (msg_type) {
@@ -707,6 +706,5 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
break;
}
}
#endif // USE_API
} // namespace esphome::api
-5
View File
@@ -30,11 +30,6 @@ APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c
APIServer::APIServer() { global_api_server = this; }
// Custom deleter defined here so `delete` sees the complete APIConnection type.
// This prevents libc++ from emitting an "incomplete type" error when other
// translation units only have the forward declaration of APIConnection.
void APIServer::APIConnectionDeleter::operator()(APIConnection *p) const { delete p; }
void APIServer::socket_failed_(const LogString *msg) {
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
this->destroy_socket_();
+2 -8
View File
@@ -193,13 +193,7 @@ class APIServer final : public Component,
// Range-for view over the populated slice [0, api_connection_count_). Read-only with respect
// to ownership — callers get `const unique_ptr&` so they can invoke non-const methods on the
// APIConnection but cannot reset/move the slot and break the count invariant.
// Custom deleter is defined out-of-line in api_server.cpp so libc++ does not
// eagerly instantiate `delete static_cast<APIConnection *>(p)` here, where
// only the forward declaration of APIConnection is visible (incomplete type).
struct APIConnectionDeleter {
void operator()(APIConnection *p) const;
};
using APIConnectionPtr = std::unique_ptr<APIConnection, APIConnectionDeleter>;
using APIConnectionPtr = std::unique_ptr<APIConnection>;
class ActiveClientsView {
const APIConnectionPtr *begin_;
const APIConnectionPtr *end_;
@@ -298,7 +292,7 @@ class APIServer final : public Component,
uint32_t last_connected_{0};
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
std::array<APIConnectionPtr, MAX_API_CONNECTIONS> clients_{};
std::array<std::unique_ptr<APIConnection>, MAX_API_CONNECTIONS> clients_{};
// Vectors and strings (12 bytes each on 32-bit)
// Shared proto write buffer for all connections.
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
-34
View File
@@ -342,32 +342,6 @@ class ProtoEncode {
}
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
}
/// Encode a 48-bit MAC address (stored in a uint64) as varint.
/// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the
/// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes
/// with no per-byte branch. Falls back to the general loop otherwise.
/// Caller must guarantee value fits in 48 bits (checked in debug builds).
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_48bit(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint64_t value) {
#ifdef ESPHOME_DEBUG_API
assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits");
#endif
// 7-byte varint holds 49 bits (7 * 7), so a 48-bit value needs all 7 bytes
// whenever bit 42 or higher is set (i.e. value >= 1 << (48 - 6)).
if (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6))) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 7);
pos[0] = static_cast<uint8_t>(value | 0x80);
pos[1] = static_cast<uint8_t>((value >> 7) | 0x80);
pos[2] = static_cast<uint8_t>((value >> 14) | 0x80);
pos[3] = static_cast<uint8_t>((value >> 21) | 0x80);
pos[4] = static_cast<uint8_t>((value >> 28) | 0x80);
pos[5] = static_cast<uint8_t>((value >> 35) | 0x80);
pos[6] = static_cast<uint8_t>(value >> 42);
pos += 7;
return;
}
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
}
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint32_t type) {
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type);
@@ -843,14 +817,6 @@ class ProtoSize {
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
return field_id_size + varint(value);
}
/// 48-bit MAC address variant: matches encode_varint_raw_48bit's fast path.
/// When any of the top 6 of 48 bits is set the encoded varint is 7 bytes;
/// otherwise fall back to the general size calculation.
/// Caller must guarantee value fits in 48 bits (encoder asserts in debug).
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_48bit_force(uint32_t field_id_size,
uint64_t value) {
return field_id_size + (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6)) ? 7 : varint(value));
}
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
}
+4 -4
View File
@@ -183,19 +183,19 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
cg.add(var.set_sensing_distance(template_))
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
template_ = await cg.templatable(selfcheck, args, cg.int_)
template_ = await cg.templatable(selfcheck, args, cg.int32)
cg.add(var.set_poweron_selfcheck_time(template_))
if protect := config.get(CONF_PROTECT_TIME):
template_ = await cg.templatable(protect, args, cg.int_)
template_ = await cg.templatable(protect, args, cg.int32)
cg.add(var.set_protect_time(template_))
if trig_base := config.get(CONF_TRIGGER_BASE):
template_ = await cg.templatable(trig_base, args, cg.int_)
template_ = await cg.templatable(trig_base, args, cg.int32)
cg.add(var.set_trigger_base(template_))
if trig_keep := config.get(CONF_TRIGGER_KEEP):
template_ = await cg.templatable(trig_keep, args, cg.int_)
template_ = await cg.templatable(trig_keep, args, cg.int32)
cg.add(var.set_trigger_keep(template_))
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
+1 -1
View File
@@ -220,7 +220,7 @@ async def to_code(config):
data = _get_data()
if data.micro_decoder_support:
add_idf_component(name="esphome/micro-decoder", ref="0.2.0")
add_idf_component(name="esphome/micro-decoder", ref="0.1.1")
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
if not data.flac_support:
+1 -1
View File
@@ -154,7 +154,7 @@ void BH1750Sensor::loop() {
break;
}
ESP_LOGV(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
this->status_clear_warning();
this->publish_state(lx);
this->state_ = IDLE;
+1 -2
View File
@@ -62,7 +62,6 @@ from esphome.const import (
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -625,7 +624,7 @@ async def setup_binary_sensor_core_(var, config):
async def register_binary_sensor(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
queue_entity_register("binary_sensor", config)
cg.add(cg.App.register_binary_sensor(var))
CORE.register_platform_component("binary_sensor", var)
await setup_binary_sensor_core_(var, config)
@@ -50,31 +50,29 @@ void MultiClickTriggerBase::on_state_(bool state) {
return;
}
// at_index_ has a value here (the !has_value() branch above returns).
size_t at_index = *this->at_index_;
if (at_index == this->timing_count_) {
if (*this->at_index_ == this->timing_count_) {
this->trigger_();
return;
}
MultiClickTriggerEvent evt = this->timing_[at_index];
MultiClickTriggerEvent evt = this->timing_[*this->at_index_];
if (evt.max_length != 4294967294UL) {
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, at_index, evt.min_length, evt.max_length); // NOLINT
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT
this->schedule_is_valid_(evt.min_length);
this->schedule_is_not_valid_(evt.max_length);
} else if (at_index + 1 != this->timing_count_) {
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
} else if (*this->at_index_ + 1 != this->timing_count_) {
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->schedule_is_valid_(evt.min_length);
} else {
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
this->is_valid_ = false;
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
}
this->at_index_ = at_index + 1;
*this->at_index_ = *this->at_index_ + 1;
}
void MultiClickTriggerBase::schedule_cooldown_() {
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
+2 -498
View File
@@ -16,7 +16,6 @@ from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7231Q,
FAMILY_BK7231T,
FAMILY_BK7238,
FAMILY_BK7251,
)
@@ -25,32 +24,16 @@ BK72XX_BOARDS = {
"name": "WB2L_M1 Wi-Fi Module",
"family": FAMILY_BK7231N,
},
"xh-wb3s": {
"name": "NiceMCU XH-WB3S",
"family": FAMILY_BK7238,
},
"cbu": {
"name": "CBU Wi-Fi Module",
"family": FAMILY_BK7231N,
},
"t1-u": {
"name": "T1-U Wi-Fi Module",
"family": FAMILY_BK7238,
},
"generic-bk7238-tuya": {
"name": "Generic - BK7238 (Tuya T1)",
"family": FAMILY_BK7238,
},
"t1-m": {
"name": "T1-M Wi-Fi Module",
"family": FAMILY_BK7238,
},
"generic-bk7231t-qfn32-tuya": {
"name": "Generic - BK7231T (Tuya)",
"name": "Generic - BK7231T (Tuya QFN32)",
"family": FAMILY_BK7231T,
},
"generic-bk7231n-qfn32-tuya": {
"name": "Generic - BK7231N (Tuya)",
"name": "Generic - BK7231N (Tuya QFN32)",
"family": FAMILY_BK7231N,
},
"cb1s": {
@@ -81,10 +64,6 @@ BK72XX_BOARDS = {
"name": "Generic - BK7252",
"family": FAMILY_BK7251,
},
"t1-3s": {
"name": "T1-3S Wi-Fi Module",
"family": FAMILY_BK7238,
},
"wb2l": {
"name": "WB2L Wi-Fi Module",
"family": FAMILY_BK7231T,
@@ -101,10 +80,6 @@ BK72XX_BOARDS = {
"name": "CB2S Wi-Fi Module",
"family": FAMILY_BK7231N,
},
"generic-bk7238": {
"name": "Generic - BK7238",
"family": FAMILY_BK7238,
},
"wa2": {
"name": "WA2 Wi-Fi Module",
"family": FAMILY_BK7231Q,
@@ -125,10 +100,6 @@ BK72XX_BOARDS = {
"name": "WB3L Wi-Fi Module",
"family": FAMILY_BK7231T,
},
"t1-2s": {
"name": "T1-2S Wi-Fi Module",
"family": FAMILY_BK7238,
},
"wb2s": {
"name": "WB2S Wi-Fi Module",
"family": FAMILY_BK7231T,
@@ -187,83 +158,6 @@ BK72XX_BOARD_PINS = {
"D12": 22,
"A0": 23,
},
"xh-wb3s": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P7": 7,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM1": 7,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 7,
"D1": 23,
"D2": 14,
"D3": 26,
"D4": 24,
"D5": 6,
"D6": 9,
"D7": 0,
"D8": 1,
"D9": 8,
"D10": 10,
"D11": 11,
"D12": 16,
"D13": 20,
"D14": 21,
"D15": 22,
"D16": 15,
"D17": 17,
"A0": 28,
"A1": 26,
"A2": 24,
"A3": 1,
"A4": 10,
"A5": 20,
},
"cbu": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
@@ -336,204 +230,6 @@ BK72XX_BOARD_PINS = {
"D18": 21,
"A0": 23,
},
"t1-u": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 14,
"D1": 16,
"D2": 23,
"D3": 22,
"D4": 20,
"D5": 1,
"D6": 0,
"D7": 24,
"D8": 9,
"D9": 26,
"D10": 6,
"D11": 8,
"D12": 11,
"D13": 10,
"D14": 28,
"D15": 21,
"D16": 17,
"D17": 15,
"A0": 20,
"A1": 1,
"A2": 24,
"A3": 26,
"A4": 10,
"A5": 28,
},
"generic-bk7238-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P7": 7,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM1": 7,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 0,
"D1": 1,
"D2": 6,
"D3": 7,
"D4": 8,
"D5": 9,
"D6": 10,
"D7": 11,
"D8": 14,
"D9": 15,
"D10": 16,
"D11": 17,
"D12": 20,
"D13": 21,
"D14": 22,
"D15": 23,
"D16": 24,
"D17": 26,
"D18": 28,
"A0": 1,
"A1": 10,
"A2": 20,
"A3": 24,
"A4": 26,
"A5": 28,
},
"t1-m": {
"WIRE2_SCL": 24,
"WIRE2_SDA": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC5": 1,
"ADC6": 10,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P24": 24,
"P26": 26,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCL2": 24,
"SDA2": 26,
"TX1": 11,
"TX2": 0,
"D0": 26,
"D1": 6,
"D2": 8,
"D3": 1,
"D4": 10,
"D5": 11,
"D6": 9,
"D7": 24,
"D11": 0,
"A0": 26,
"A1": 10,
"A2": 1,
"A3": 24,
},
"generic-bk7231t-qfn32-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
@@ -1085,75 +781,6 @@ BK72XX_BOARD_PINS = {
"A6": 12,
"A7": 13,
},
"t1-3s": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 20,
"D1": 22,
"D2": 6,
"D3": 8,
"D4": 9,
"D5": 23,
"D6": 0,
"D7": 1,
"D8": 24,
"D9": 26,
"D10": 10,
"D11": 11,
"D12": 17,
"D13": 16,
"D14": 15,
"D15": 14,
"A0": 20,
"A1": 1,
"A2": 24,
"A3": 26,
"A4": 10,
},
"wb2l": {
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
@@ -1338,84 +965,6 @@ BK72XX_BOARD_PINS = {
"D10": 21,
"A0": 23,
},
"generic-bk7238": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P7": 7,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM1": 7,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 0,
"D1": 1,
"D2": 6,
"D3": 7,
"D4": 8,
"D5": 9,
"D6": 10,
"D7": 11,
"D8": 14,
"D9": 15,
"D10": 16,
"D11": 17,
"D12": 20,
"D13": 21,
"D14": 22,
"D15": 23,
"D16": 24,
"D17": 26,
"D18": 28,
"A0": 1,
"A1": 10,
"A2": 20,
"A3": 24,
"A4": 26,
"A5": 28,
},
"wa2": {
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
@@ -1686,51 +1235,6 @@ BK72XX_BOARD_PINS = {
"D15": 1,
"A0": 23,
},
"t1-2s": {
"WIRE2_SCL": 24,
"WIRE2_SDA": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC5": 1,
"ADC6": 10,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P24": 24,
"P26": 26,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCL2": 24,
"SDA2": 26,
"TX1": 11,
"TX2": 0,
"D0": 26,
"D1": 6,
"D2": 8,
"D3": 1,
"D4": 10,
"D5": 11,
"D6": 9,
"D7": 24,
"D11": 0,
"A0": 26,
"A1": 10,
"A2": 1,
"A3": 24,
},
"wb2s": {
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
@@ -30,6 +30,19 @@ void BluetoothProxy::setup() {
this->configured_scan_active_ = this->parent_->get_scan_active();
this->parent_->add_scanner_state_listener(this);
this->set_interval(100, [this]() {
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
this->flush_pending_advertisements_();
return;
}
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect();
}
}
});
}
void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) {
@@ -120,25 +133,6 @@ void BluetoothProxy::dump_config() {
YESNO(this->active_), this->connection_count_);
}
void BluetoothProxy::loop() {
// Run advertisement flush / connection cleanup every 100ms
uint32_t now = App.get_loop_component_start_time();
if (now - this->last_advertisement_flush_time_ < 100)
return;
this->last_advertisement_flush_time_ = now;
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
this->flush_pending_advertisements_();
return;
}
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect();
}
}
}
esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() {
return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
}
@@ -207,6 +201,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE);
this->log_connection_info_(connection, "v3 without cache");
}
uint64_to_bd_addr(msg.address, connection->remote_bda_);
connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type));
connection->set_state(espbt::ClientState::DISCOVERED);
this->send_connections_free();
@@ -65,7 +65,6 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override;
void dump_config() override;
void setup() override;
void loop() override;
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
void register_connection(BluetoothConnection *connection) {
@@ -177,9 +176,6 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
// BLE advertisement batching
api::BluetoothLERawAdvertisementsResponse response_;
// Group 3: 4-byte types
uint32_t last_advertisement_flush_time_{0};
// Pre-allocated response message - always ready to send
api::BluetoothConnectionsFreeResponse connections_free_response_;
+27 -27
View File
@@ -78,43 +78,43 @@ void BME680Component::setup() {
}
// Read calibration
uint8_t coeff1[25];
if (!this->read_bytes(BME680_REGISTER_COEFF1, coeff1, 25)) {
uint8_t cal1[25];
if (!this->read_bytes(BME680_REGISTER_COEFF1, cal1, 25)) {
this->mark_failed();
return;
}
uint8_t coeff2[16];
if (!this->read_bytes(BME680_REGISTER_COEFF2, coeff2, 16)) {
uint8_t cal2[16];
if (!this->read_bytes(BME680_REGISTER_COEFF2, cal2, 16)) {
this->mark_failed();
return;
}
this->calibration_.t1 = coeff2[9] << 8 | coeff2[8];
this->calibration_.t2 = coeff1[2] << 8 | coeff1[1];
this->calibration_.t3 = coeff1[3];
this->calibration_.t1 = cal2[9] << 8 | cal2[8];
this->calibration_.t2 = cal1[2] << 8 | cal1[1];
this->calibration_.t3 = cal1[3];
this->calibration_.h1 = coeff2[2] << 4 | (coeff2[1] & 0x0F);
this->calibration_.h2 = coeff2[0] << 4 | coeff2[1] >> 4;
this->calibration_.h3 = coeff2[3];
this->calibration_.h4 = coeff2[4];
this->calibration_.h5 = coeff2[5];
this->calibration_.h6 = coeff2[6];
this->calibration_.h7 = coeff2[7];
this->calibration_.h1 = cal2[2] << 4 | (cal2[1] & 0x0F);
this->calibration_.h2 = cal2[0] << 4 | cal2[1] >> 4;
this->calibration_.h3 = cal2[3];
this->calibration_.h4 = cal2[4];
this->calibration_.h5 = cal2[5];
this->calibration_.h6 = cal2[6];
this->calibration_.h7 = cal2[7];
this->calibration_.p1 = coeff1[6] << 8 | coeff1[5];
this->calibration_.p2 = coeff1[8] << 8 | coeff1[7];
this->calibration_.p3 = coeff1[9];
this->calibration_.p4 = coeff1[12] << 8 | coeff1[11];
this->calibration_.p5 = coeff1[14] << 8 | coeff1[13];
this->calibration_.p6 = coeff1[16];
this->calibration_.p7 = coeff1[15];
this->calibration_.p8 = coeff1[20] << 8 | coeff1[19];
this->calibration_.p9 = coeff1[22] << 8 | coeff1[21];
this->calibration_.p10 = coeff1[23];
this->calibration_.p1 = cal1[6] << 8 | cal1[5];
this->calibration_.p2 = cal1[8] << 8 | cal1[7];
this->calibration_.p3 = cal1[9];
this->calibration_.p4 = cal1[12] << 8 | cal1[11];
this->calibration_.p5 = cal1[14] << 8 | cal1[13];
this->calibration_.p6 = cal1[16];
this->calibration_.p7 = cal1[15];
this->calibration_.p8 = cal1[20] << 8 | cal1[19];
this->calibration_.p9 = cal1[22] << 8 | cal1[21];
this->calibration_.p10 = cal1[23];
this->calibration_.gh1 = coeff2[14];
this->calibration_.gh2 = coeff2[12] << 8 | coeff2[13];
this->calibration_.gh3 = coeff2[15];
this->calibration_.gh1 = cal2[14];
this->calibration_.gh2 = cal2[12] << 8 | cal2[13];
this->calibration_.gh3 = cal2[15];
uint8_t temp_var = 0;
if (!this->read_byte(0x02, &temp_var)) {
+1 -2
View File
@@ -19,7 +19,6 @@ from esphome.const import (
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -102,7 +101,7 @@ async def setup_button_core_(var, config):
async def register_button(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
queue_entity_register("button", config)
cg.add(cg.App.register_button(var))
CORE.register_platform_component("button", var)
await setup_button_core_(var, config)
+2 -6
View File
@@ -49,11 +49,7 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
IS_PLATFORM_COMPONENT = True
@@ -446,7 +442,7 @@ async def setup_climate_core_(var, config):
async def register_climate(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
queue_entity_register("climate", config)
cg.add(cg.App.register_climate(var))
CORE.register_platform_component("climate", var)
await setup_climate_core_(var, config)
+1 -2
View File
@@ -39,7 +39,6 @@ from esphome.const import (
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -233,7 +232,7 @@ async def setup_cover_core_(var, config):
async def register_cover(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
queue_entity_register("cover", config)
cg.add(cg.App.register_cover(var))
CORE.register_platform_component("cover", var)
await setup_cover_core_(var, config)
+2 -6
View File
@@ -22,11 +22,7 @@ from esphome.const import (
CONF_YEAR,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@rfdarter", "@jesserockz"]
@@ -164,7 +160,7 @@ async def register_datetime(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
entity_type = config[CONF_TYPE].lower()
queue_entity_register(entity_type, config)
cg.add(getattr(cg.App, f"register_{entity_type}")(var))
CORE.register_platform_component(entity_type, var)
await setup_datetime_core_(var, config)
+5 -8
View File
@@ -193,14 +193,11 @@ def _validate_ex1_wakeup_mode(value):
def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod:
if CORE.is_bk72xx:
max_duration = core.TimePeriod(hours=36)
if value > max_duration:
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
elif CORE.using_zephyr:
max_duration = core.TimePeriod(days=49)
if value > max_duration:
raise cv.Invalid("sleep duration cannot be more than 49 days on Zephyr")
if not CORE.is_bk72xx:
return value
max_duration = core.TimePeriod(hours=36)
if value > max_duration:
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
return value
@@ -9,11 +9,18 @@ static const char *const TAG = "deep_sleep";
// 5 seconds for deep sleep to ensure clean disconnect from Home Assistant
static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000;
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
std::atomic<DeepSleepComponent *> global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void DeepSleepComponent::setup() {
#ifdef USE_ZEPHYR
k_sem_init(&this->wakeup_sem_, 0, 1);
#endif
global_has_deep_sleep = true;
this->schedule_sleep_();
// It can be used from another thread for waking up the device.
// It should be called as last item in setup.
global_deep_sleep.store(this);
}
void DeepSleepComponent::schedule_sleep_() {
@@ -4,6 +4,8 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <atomic>
#ifdef USE_ESP32
#include <esp_sleep.h>
#endif
@@ -13,6 +15,10 @@
#include "esphome/core/time.h"
#endif
#ifdef USE_ZEPHYR
#include <zephyr/kernel.h>
#endif
#include <cinttypes>
namespace esphome {
@@ -119,6 +125,9 @@ class DeepSleepComponent : public Component {
void prevent_deep_sleep();
void allow_deep_sleep();
#ifdef USE_ZEPHYR
void wakeup();
#endif
protected:
// Returns nullopt if no run duration is set. Otherwise, returns the run
@@ -158,6 +167,9 @@ class DeepSleepComponent : public Component {
optional<uint32_t> run_duration_;
bool next_enter_deep_sleep_{false};
bool prevent_{false};
#ifdef USE_ZEPHYR
k_sem wakeup_sem_;
#endif
};
extern bool global_has_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -244,5 +256,8 @@ template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, publ
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); }
};
extern std::atomic<DeepSleepComponent *>
global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace deep_sleep
} // namespace esphome
@@ -1,13 +1,17 @@
#include "deep_sleep_component.h"
#ifdef USE_ZEPHYR
#include "esphome/core/log.h"
#include "esphome/core/wake.h"
#include <zephyr/sys/poweroff.h>
#include <zephyr/kernel.h>
#include <zephyr/stats/stats.h>
#include <zephyr/pm/pm.h>
namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep";
void DeepSleepComponent::wakeup() { k_sem_give(&this->wakeup_sem_); }
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
void DeepSleepComponent::dump_config_platform_() {}
@@ -15,8 +19,9 @@ void DeepSleepComponent::dump_config_platform_() {}
bool DeepSleepComponent::prepare_to_sleep_() { return true; }
void DeepSleepComponent::deep_sleep_() {
k_timeout_t sleep_duration = K_FOREVER;
if (this->sleep_duration_.has_value()) {
esphome::internal::wakeable_delay(static_cast<uint32_t>(*this->sleep_duration_ / 1000));
sleep_duration = K_USEC(*this->sleep_duration_);
} else {
#ifndef USE_ZIGBEE
// the device can be woken up through one of the following signals:
@@ -28,12 +33,11 @@ void DeepSleepComponent::deep_sleep_() {
//
// The system is reset when it wakes up from System OFF mode.
sys_poweroff();
#else
esphome::internal::wakeable_delay(UINT32_MAX);
#endif
}
const bool woke = esphome::wake_request_take();
if (woke) {
// It might wake up immediately if k_sem_give was called again after wake up
int ret = k_sem_take(&this->wakeup_sem_, sleep_duration);
if (ret == 0) {
ESP_LOGD(TAG, "Woken up by another thread");
} else {
ESP_LOGD(TAG, "Timeout expired (normal sleep)");
@@ -29,7 +29,7 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component {
protected:
void control(const AlarmControlPanelCall &call) override {
auto state = call.get_state().value_or(ACP_STATE_DISARMED);
const auto &code = call.get_code();
auto code = call.get_code();
switch (state) {
case ACP_STATE_ARMED_AWAY:
if (this->get_requires_code_to_arm()) {
@@ -104,9 +104,8 @@ int8_t CircularCommandQueue::enqueue(std::unique_ptr<Command> cmd) {
if (this->is_full()) {
ESP_LOGE(TAG, "Command queue is full");
return -1;
} else if (this->is_empty()) {
} else if (this->is_empty())
front_++;
}
rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE;
commands_[rear_] = std::move(cmd); // Transfer ownership using std::move
return 1;
-5
View File
@@ -1724,11 +1724,6 @@ async def to_code(config):
CORE.relative_internal_path(".espressif")
)
# Both ESP-IDF and ESP32 Arduino builds generate IDF app metadata. Keep
# volatile build path/time data out of the binary so equivalent projects can
# produce reproducible outputs and downstream tooling can reuse artifacts.
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
+10 -1
View File
@@ -22,7 +22,7 @@ extern "C" __attribute__((weak)) void initArduino() {}
namespace esphome {
// yield(), delay(), micros(), millis_64() inlined in hal.h.
void HOT yield() { vPortYield(); }
// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig),
// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because
// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32.
@@ -37,6 +37,15 @@ uint32_t IRAM_ATTR HOT millis() {
return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time()));
#endif
}
// millis_64() stays on esp_timer — a different clock from xTaskGetTickCount(). This is
// safe because the two are never cross-compared: millis() values are only used for
// millis()-vs-millis() deltas (feed_wdt, warn_blocking, component start time), while
// millis_64() is used by the Scheduler and uptime sensors. On ESP32 (USE_NATIVE_64BIT_TIME),
// Scheduler::millis_64_from_(now) discards the 32-bit now and calls millis_64() directly,
// so the Scheduler is internally consistent on the esp_timer clock.
uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); }
void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
esp_restart();
+1 -17
View File
@@ -18,12 +18,6 @@ struct NVSData {
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// open() runs from app_main() before the logger is initialized, so any failure
// must be deferred until after global_logger is set. This is emitted from the
// first make_preference() call, which runs from the generated setup() after
// log->pre_setup() has run at EARLY_INIT priority.
static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
@@ -76,14 +70,12 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
}
void ESP32Preferences::open() {
// Runs from app_main() before the logger is initialized; any logging here
// must be deferred. See s_open_err and make_preference() below.
nvs_flash_init();
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
if (err == 0)
return;
s_open_err = err;
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
nvs_flash_deinit();
nvs_flash_erase();
nvs_flash_init();
@@ -95,14 +87,6 @@ void ESP32Preferences::open() {
}
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
if (s_open_err != ESP_OK) {
if (this->nvs_handle == 0) {
ESP_LOGW(TAG, "nvs_open failed: %s - NVS unavailable", esp_err_to_name(s_open_err));
} else {
ESP_LOGW(TAG, "nvs_open failed: %s - erased NVS", esp_err_to_name(s_open_err));
}
s_open_err = ESP_OK;
}
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = this->nvs_handle;
pref->key = type;
+1 -1
View File
@@ -104,7 +104,7 @@ ESPBTUUID ESPBTUUID::as_128bit() const {
} else {
uuid32 = this->uuid_.uuid.uuid16;
}
for (uint16_t i = 0; i < this->uuid_.len; i++) {
for (uint8_t i = 0; i < this->uuid_.len; i++) {
data[12 + i] = ((uuid32 >> i * 8) & 0xFF);
}
return ESPBTUUID::from_raw(data);
@@ -166,9 +166,8 @@ void ESP32BLETracker::loop() {
ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts;
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d, active: %d",
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
this->client_state_counts_.disconnecting, this->client_state_counts_.active);
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting,
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
}
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
@@ -191,18 +190,10 @@ void ESP32BLETracker::loop() {
*/
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
// no clients are in the transient CONNECTING / DISCOVERED / DISCONNECTING states
// (their state changes increment version when they finish). CONNECTED / ESTABLISHED
// clients do NOT block this branch — the coex revert below has its own active-count gate.
// all clients are idle (their state changes increment version when they finish)
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
// Only revert to BALANCE when no connections are active. Established connections
// continue to need PREFER_BT so peer GATT responses can reach us while WiFi traffic
// (advertisement upload, log streaming) competes for the shared radio. Reverting too
// early causes Bluedroid to time out at ~20s and synthesize status=133.
if (!counts.active) {
this->update_coex_preference_(false);
}
this->update_coex_preference_(false);
#endif
if (this->scan_continuous_) {
this->start_scan_(false); // first = false
@@ -710,10 +701,9 @@ void ESP32BLETracker::dump_config() {
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
ESP_LOGCONFIG(TAG,
" Scanner State: %s\n"
" Connecting: %d, discovered: %d, disconnecting: %d, active: %d",
" Connecting: %d, discovered: %d, disconnecting: %d",
this->scanner_state_to_string_(this->scanner_state_), this->client_state_counts_.connecting,
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting,
this->client_state_counts_.active);
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
if (this->scan_start_fail_count_) {
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
}
@@ -160,13 +160,9 @@ struct ClientStateCounts {
uint8_t connecting = 0;
uint8_t discovered = 0;
uint8_t disconnecting = 0;
// CONNECTED + ESTABLISHED clients. Tracked so coex stays at PREFER_BT
// while active connections may still need to send/receive GATT traffic.
uint8_t active = 0;
bool operator==(const ClientStateCounts &other) const {
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting &&
active == other.active;
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting;
}
bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
@@ -385,10 +381,6 @@ class ESP32BLETracker : public Component,
case ClientState::CONNECTING:
counts.connecting++;
break;
case ClientState::CONNECTED:
case ClientState::ESTABLISHED:
counts.active++;
break;
default:
break;
}
@@ -216,7 +216,6 @@ void ESP32TouchComponent::setup() {
// Do initial oneshot scans to populate baseline values
for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) {
err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS);
App.feed_wdt(); // 3 scans with 2s timeout might exceed WDT, so feed it here to be safe
if (err != ESP_OK) {
ESP_LOGW(TAG, "Oneshot scan %" PRIu32 " failed: %s", i, esp_err_to_name(err));
}
-5
View File
@@ -314,11 +314,6 @@ async def to_code(config):
for symbol in ("vprintf", "printf", "fprintf"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# Wrap Arduino's millis() so all callers (including Arduino libraries and ISR
# handlers) use our fast accumulator instead of the expensive 4x 64-bit multiply
# implementation in the Arduino ESP8266 core.
cg.add_build_flag("-Wl,--wrap=millis")
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
+5 -77
View File
@@ -15,75 +15,11 @@ extern "C" {
namespace esphome {
// yield(), micros(), millis_64() inlined in hal.h.
// Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit
// multiplies on the LX106). Tracks a running ms counter from 32-bit
// system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis
// (via -Wl,--wrap=millis) so Arduino libs and IRAM_ATTR ISR handlers (e.g.
// Wiegand, ZyAura) also get the fast version. xt_rsil(15) guards the static
// state against ISR re-entry; the critical section is bounded (≤10 while-loop
// iterations, ~100 ns on the common path, or a constant-time /1000 ~2.5 μs on
// the rare path — well under WiFi's ~10 μs ISR latency budget). NMIs (level
// >15) are not masked, but the ESP8266 SDK's NMI handlers don't call millis().
//
// system_get_time() wraps every ~71.6 min; unsigned (now_us - last_us) handles
// one wrap. The main loop calls millis() at 60+ Hz, so delta stays tiny — a
// >71 min block would trip the watchdog long before it could matter here.
static constexpr uint32_t MILLIS_RARE_PATH_THRESHOLD_US = 10000;
static constexpr uint32_t US_PER_MS = 1000;
uint32_t IRAM_ATTR HOT millis() {
// Struct packs the three statics so the compiler loads one base address
// instead of three separate literal pool entries (saves ~8 bytes IRAM).
static struct {
uint32_t cache;
uint32_t remainder;
uint32_t last_us;
} state = {0, 0, 0};
uint32_t ps = xt_rsil(15);
uint32_t now_us = system_get_time();
uint32_t delta = now_us - state.last_us;
state.last_us = now_us;
state.remainder += delta;
if (state.remainder >= MILLIS_RARE_PATH_THRESHOLD_US) {
// Rare path: large gap (WiFi scan, boot, long block). Constant-time
// conversion keeps the critical section bounded.
uint32_t ms = state.remainder / US_PER_MS;
state.cache += ms;
// Reuse ms instead of `remainder %= US_PER_MS` — `%` would compile to a
// second __umodsi3 call on the LX106 (no hardware divide).
state.remainder -= ms * US_PER_MS;
} else {
// Common path: small gap. At most ~10 iterations since remainder was
// < threshold (10 ms) on entry and delta adds at most one more threshold
// before exiting this branch.
while (state.remainder >= US_PER_MS) {
state.cache++;
state.remainder -= US_PER_MS;
}
}
uint32_t result = state.cache;
xt_wsr_ps(ps);
return result;
}
// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object
// call to the original millis() that --wrap can't intercept, so calling ::delay()
// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still
// enters esp_schedule()/esp_suspend_within_cont() via yield(), so SDK tasks and
// WiFi run correctly. Theoretically less power-efficient than Arduino's
// os_timer-based delay() for long waits, but nearly all ESPHome delays are short
// (sensor/I²C/SPI settling in the 1100 ms range) where the difference is
// negligible.
void HOT delay(uint32_t ms) {
if (ms == 0) {
optimistic_yield(1000);
return;
}
uint32_t start = millis();
while (millis() - start < ms) {
optimistic_yield(1000);
}
}
void HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
void HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
system_restart();
@@ -142,12 +78,4 @@ extern "C" void resetPins() { // NOLINT
} // namespace esphome
// Linker wrap: redirect all ::millis() calls (Arduino libs, ISRs) to our accumulator.
// Requires -Wl,--wrap=millis in build flags (added by __init__.py).
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" uint32_t IRAM_ATTR __wrap_millis() { return esphome::millis(); }
// Note: Arduino's init() registers a 60-second overflow timer for micros64().
// We leave it running — wrapping init() as a no-op would break micros64()'s
// overflow tracking, and the timer's cost is negligible (~3 μs per 60 s).
#endif // USE_ESP8266
+1 -13
View File
@@ -292,7 +292,6 @@ void ESPHomeOTAComponent::handle_data_() {
bool update_started = false;
size_t total = 0;
uint32_t last_progress = 0;
uint32_t last_data_ms = 0;
uint8_t buf[OTA_BUFFER_SIZE];
char *sbuf = reinterpret_cast<char *>(buf);
size_t ota_size;
@@ -351,18 +350,8 @@ void ESPHomeOTAComponent::handle_data_() {
// Acknowledge MD5 OK - 1 byte
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
// Track when we last received data so a silently-vanished peer (no FIN/RST
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
// can't wedge the device indefinitely. Without this, the loop only exits
// on actual data, EOF, or a non-EWOULDBLOCK error from read(), and lwIP
// TCP keepalive isn't enabled here.
last_data_ms = millis();
while (total < ota_size) {
if (millis() - last_data_ms > OTA_SOCKET_TIMEOUT_DATA) {
ESP_LOGW(TAG, "No data received for %u ms", (unsigned) OTA_SOCKET_TIMEOUT_DATA);
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
// TODO: timeout check
size_t remaining = ota_size - total;
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
ssize_t read = this->client_->read(buf, requested);
@@ -380,7 +369,6 @@ void ESPHomeOTAComponent::handle_data_() {
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
last_data_ms = millis();
error_code = this->backend_->write(buf, read);
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Flash write err %d", error_code);
+1 -2
View File
@@ -19,7 +19,6 @@ from esphome.const import (
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -109,7 +108,7 @@ async def setup_event_core_(var, config, *, event_types: list[str]):
async def register_event(var, config, *, event_types: list[str]):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
queue_entity_register("event", config)
cg.add(cg.App.register_event(var))
CORE.register_platform_component("event", var)
await setup_event_core_(var, config, event_types=event_types)
@@ -1,6 +1,5 @@
import logging
from pathlib import Path
from typing import Any
from esphome import git, loader
import esphome.config_validation as cv
@@ -18,7 +17,7 @@ from esphome.const import (
TYPE_GIT,
TYPE_LOCAL,
)
from esphome.core import CORE, TimePeriodSeconds
from esphome.core import CORE
_LOGGER = logging.getLogger(__name__)
@@ -36,15 +35,17 @@ CONFIG_SCHEMA = cv.ensure_list(
)
async def to_code(config: dict[str, Any]) -> None:
async def to_code(config):
pass
def _process_git_config(config: dict[str, Any], refresh: TimePeriodSeconds) -> Path:
def _process_git_config(config: dict, refresh, skip_update: bool = False) -> str:
# When skip_update is True, use NEVER_REFRESH to prevent updates
actual_refresh = git.NEVER_REFRESH if skip_update else refresh
repo_dir, _ = git.clone_or_update(
url=config[CONF_URL],
ref=config.get(CONF_REF),
refresh=refresh,
refresh=actual_refresh,
domain=DOMAIN,
username=config.get(CONF_USERNAME),
password=config.get(CONF_PASSWORD),
@@ -71,12 +72,12 @@ def _process_git_config(config: dict[str, Any], refresh: TimePeriodSeconds) -> P
return components_dir
def _process_single_config(config: dict[str, Any]) -> None:
def _process_single_config(config: dict, skip_update: bool = False):
conf = config[CONF_SOURCE]
if conf[CONF_TYPE] == TYPE_GIT:
with cv.prepend_path([CONF_SOURCE]):
components_dir = _process_git_config(
config[CONF_SOURCE], config[CONF_REFRESH]
config[CONF_SOURCE], config[CONF_REFRESH], skip_update
)
elif conf[CONF_TYPE] == TYPE_LOCAL:
components_dir = Path(CORE.relative_config_path(conf[CONF_PATH]))
@@ -106,7 +107,7 @@ def _process_single_config(config: dict[str, Any]) -> None:
loader.install_meta_finder(components_dir, allowed_components=allowed_components)
def do_external_components_pass(config: dict[str, Any]) -> None:
def do_external_components_pass(config: dict, skip_update: bool = False) -> None:
conf = config.get(DOMAIN)
if conf is None:
return
@@ -114,4 +115,4 @@ def do_external_components_pass(config: dict[str, Any]) -> None:
conf = CONFIG_SCHEMA(conf)
for i, c in enumerate(conf):
with cv.prepend_path(i):
_process_single_config(c)
_process_single_config(c, skip_update)
+2 -2
View File
@@ -35,7 +35,7 @@ void EZOSensor::update() {
}
if (!found) {
auto ezo_command = make_unique<EzoCommand>();
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
ezo_command->command = "R";
ezo_command->command_type = EzoCommandType::EZO_READ;
ezo_command->delay_ms = 900;
@@ -162,7 +162,7 @@ void EZOSensor::loop() {
}
void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) {
auto ezo_command = make_unique<EzoCommand>();
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
ezo_command->command = command;
ezo_command->command_type = command_type;
ezo_command->delay_ms = delay_ms;
+2 -6
View File
@@ -32,11 +32,7 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
IS_PLATFORM_COMPONENT = True
@@ -296,7 +292,7 @@ async def setup_fan_core_(var, config):
async def register_fan(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
queue_entity_register("fan", config)
cg.add(cg.App.register_fan(var))
CORE.register_platform_component("fan", var)
await setup_fan_core_(var, config)
+10 -10
View File
@@ -3,12 +3,11 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::feedback {
namespace esphome {
namespace feedback {
static const char *const TAG = "feedback.cover";
static constexpr uint32_t DIRECTION_CHANGE_TIMEOUT_ID = 1;
using namespace esphome::cover;
void FeedbackCover::setup() {
@@ -38,7 +37,7 @@ void FeedbackCover::setup() {
}
#endif
this->last_recompute_time_ = this->start_dir_time_ = App.get_loop_component_start_time();
this->last_recompute_time_ = this->start_dir_time_ = millis();
}
CoverTraits FeedbackCover::get_traits() {
@@ -136,7 +135,7 @@ void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop
#endif
void FeedbackCover::endstop_reached_(bool open_endstop) {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
@@ -175,7 +174,7 @@ void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool
if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
#endif
{
const uint32_t now = App.get_loop_component_start_time();
auto now = millis();
this->current_operation = operation;
this->start_dir_time_ = this->last_recompute_time_ = now;
this->publish_state();
@@ -307,7 +306,7 @@ void FeedbackCover::control(const CoverCall &call) {
void FeedbackCover::stop_prev_trigger_() {
if (this->direction_change_waittime_.has_value()) {
this->cancel_timeout(DIRECTION_CHANGE_TIMEOUT_ID);
this->cancel_timeout("direction_change");
}
if (this->prev_command_trigger_ != nullptr) {
this->prev_command_trigger_->stop_action();
@@ -378,7 +377,7 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
this->start_direction_(COVER_OPERATION_IDLE);
this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, *this->direction_change_waittime_,
this->set_timeout("direction_change", *this->direction_change_waittime_,
[this, dir]() { this->start_direction_(dir); });
} else {
@@ -396,7 +395,7 @@ void FeedbackCover::recompute_position_() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
float dir;
float action_dur;
float min_pos;
@@ -452,4 +451,5 @@ void FeedbackCover::recompute_position_() {
this->last_recompute_time_ = now;
}
} // namespace esphome::feedback
} // namespace feedback
} // namespace esphome
+4 -2
View File
@@ -8,7 +8,8 @@
#endif
#include "esphome/components/cover/cover.h"
namespace esphome::feedback {
namespace esphome {
namespace feedback {
class FeedbackCover : public cover::Cover, public Component {
public:
@@ -84,4 +85,5 @@ class FeedbackCover : public cover::Cover, public Component {
uint32_t update_interval_{1000};
};
} // namespace esphome::feedback
} // namespace feedback
} // namespace esphome
@@ -210,9 +210,8 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
#ifdef USE_WIFI
else if (this->send_wifi_signal_ &&
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) {
SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
}
#endif
} break;
default:
+7 -2
View File
@@ -8,6 +8,7 @@
#include <csignal>
#include <sched.h>
#include <time.h>
#include <cmath>
#include <cstdlib>
namespace {
@@ -21,7 +22,9 @@ void HOT yield() { ::sched_yield(); }
uint32_t IRAM_ATTR HOT millis() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);
return static_cast<uint32_t>(spec.tv_sec * 1000ULL + spec.tv_nsec / 1000000);
time_t seconds = spec.tv_sec;
uint32_t ms = round(spec.tv_nsec / 1e6);
return ((uint32_t) seconds) * 1000U + ms;
}
uint64_t millis_64() {
struct timespec spec;
@@ -40,7 +43,9 @@ void HOT delay(uint32_t ms) {
uint32_t IRAM_ATTR HOT micros() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);
return static_cast<uint32_t>(spec.tv_sec * 1000000ULL + spec.tv_nsec / 1000);
time_t seconds = spec.tv_sec;
uint32_t us = round(spec.tv_nsec / 1e3);
return ((uint32_t) seconds) * 1000000U + us;
}
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) {
struct timespec ts;
@@ -462,7 +462,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
this->request_headers_.push_back({key, value});
}
void add_collect_header(const char *value) { this->lower_case_collect_headers_.emplace_back(value); }
void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); }
void init_json(size_t count) { this->json_.init(count); }
void add_json(const char *key, TemplatableValue<std::string, Ts...> value) { this->json_.push_back({key, value}); }
+1 -1
View File
@@ -5,7 +5,7 @@
#include "i2c_bus.h"
#include "esphome/core/component.h"
struct device; // NOLINT(readability-identifier-naming) - forward decl of Zephyr's device type
struct device;
namespace esphome::i2c {
+7 -14
View File
@@ -744,28 +744,21 @@ async def write_image(config, all_frames=False):
if frame_count <= 1:
_LOGGER.warning("Image file %s has no animation frames", path)
# Encode each frame with its own encoder and concatenate. This keeps every
# frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane]
# per frame) so animation frame stepping in image.cpp / animation.cpp stays
# correct without needing to know the total frame count.
byte_order = config.get(CONF_BYTE_ORDER)
combined_data: list[int] = []
encoder: ImageEncoder | None = None
total_rows = height * frame_count
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
if byte_order := config.get(CONF_BYTE_ORDER):
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
for frame_index in range(frame_count):
image.seek(frame_index)
encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha)
if byte_order is not None:
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
pixels = encoder.convert(image.resize((width, height)), path).getdata()
for row in range(height):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
encoder.end_image()
combined_data.extend(encoder.data)
encoder.end_image()
rhs = [HexInt(x) for x in combined_data]
rhs = [HexInt(x) for x in encoder.data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
image_type = get_image_type_enum(type)
trans_value = get_transparency_enum(encoder.transparency)
+2 -2
View File
@@ -12,7 +12,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import queue_entity_register, setup_entity
from esphome.core.entity_helpers import setup_entity
from esphome.coroutine import CoroPriority
from esphome.types import ConfigType
@@ -54,8 +54,8 @@ async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None:
"""Register an infrared device with the core."""
cg.add_define("USE_IR_RF")
await cg.register_component(var, config)
queue_entity_register("infrared", config)
await setup_infrared_core_(var, config)
cg.add(cg.App.register_infrared(var))
CORE.register_platform_component("infrared", var)
@@ -41,12 +41,12 @@ bool InkbirdIbstH1Mini::parse_device(const esp32_ble_tracker::ESPBTDevice &devic
ESP_LOGVV(TAG, "parse_device(): service_data is expected to be empty");
return false;
}
const auto &mnf_datas = device.get_manufacturer_datas();
auto mnf_datas = device.get_manufacturer_datas();
if (mnf_datas.size() != 1) {
ESP_LOGVV(TAG, "parse_device(): manufacturer_datas is expected to have a single element");
return false;
}
const auto &mnf_data = mnf_datas[0];
auto mnf_data = mnf_datas[0];
if (mnf_data.uuid.get_uuid().len != ESP_UUID_LEN_16) {
ESP_LOGVV(TAG, "parse_device(): manufacturer data element is expected to have uuid of length 16");
return false;
+1 -111
View File
@@ -1,73 +1,13 @@
#include "ir_rf_proxy.h"
#include <cinttypes>
#include "esphome/core/log.h"
namespace esphome::ir_rf_proxy {
static const char *const TAG = "ir_rf_proxy";
// ========== Shared transmit helper ==========
// Static template: all instantiations occur in this translation unit.
template<typename CallT>
static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter, uint32_t carrier_frequency,
const CallT &call) {
if (transmitter == nullptr) {
ESP_LOGW(TAG, "No transmitter configured");
return;
}
if (!call.has_raw_timings()) {
ESP_LOGE(TAG, "No raw timings provided");
return;
}
auto transmit_call = transmitter->transmit();
auto *transmit_data = transmit_call.get_data();
transmit_data->set_carrier_frequency(carrier_frequency);
if (call.is_packed()) {
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
call.get_packed_count());
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(),
call.get_repeat_count());
} else if (call.is_base64url()) {
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url data");
return;
}
constexpr int32_t max_timing_us = 500000;
for (int32_t timing : transmit_data->get_data()) {
int32_t abs_timing = timing < 0 ? -timing : timing;
if (abs_timing > max_timing_us) {
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
return;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
call.get_repeat_count());
} else {
transmit_data->set_data(call.get_raw_timings());
ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(),
call.get_repeat_count());
}
if (call.get_repeat_count() > 0) {
transmit_call.set_send_times(call.get_repeat_count());
}
transmit_call.perform();
}
// ========== IrRfProxy (Infrared platform) ==========
#ifdef USE_IR_RF
void IrRfProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"IR Proxy '%s'\n"
"IR/RF Proxy '%s'\n"
" Supports Transmitter: %s\n"
" Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
@@ -80,54 +20,4 @@ void IrRfProxy::dump_config() {
}
}
void IrRfProxy::control(const infrared::InfraredCall &call) {
uint32_t carrier = call.get_carrier_frequency().value_or(0);
transmit_raw_timings(this->transmitter_, carrier, call);
}
#endif // USE_IR_RF
// ========== RfProxy (Radio Frequency platform) ==========
#ifdef USE_RADIO_FREQUENCY
void RfProxy::setup() {
this->traits_.set_supports_transmitter(this->transmitter_ != nullptr);
this->traits_.set_supports_receiver(this->receiver_ != nullptr);
// remote_transmitter/receiver always uses OOK (on-off keying)
this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK);
if (this->receiver_ != nullptr) {
this->receiver_->register_listener(this);
}
}
void RfProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"RF Proxy '%s'\n"
" Backend: remote_transmitter/receiver\n"
" Supports Transmitter: %s\n"
" Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
YESNO(this->traits_.get_supports_receiver()));
const auto &traits = this->traits_;
if (traits.get_frequency_min_hz() > 0) {
if (traits.get_frequency_min_hz() == traits.get_frequency_max_hz()) {
ESP_LOGCONFIG(TAG, " Frequency: %.3f MHz (fixed)", traits.get_frequency_min_hz() / 1e6f);
} else {
ESP_LOGCONFIG(TAG, " Frequency Range: %.3f - %.3f MHz", traits.get_frequency_min_hz() / 1e6f,
traits.get_frequency_max_hz() / 1e6f);
}
}
}
void RfProxy::control(const radio_frequency::RadioFrequencyCall &call) {
// RF: no IR carrier modulation
transmit_raw_timings(this->transmitter_, 0, call);
}
#endif // USE_RADIO_FREQUENCY
} // namespace esphome::ir_rf_proxy
@@ -4,19 +4,10 @@
// without following the normal breaking changes policy. Use at your own risk.
// Once the API is considered stable, this warning will be removed.
#include "esphome/components/remote_base/remote_base.h"
#ifdef USE_IR_RF
#include "esphome/components/infrared/infrared.h"
#endif
#ifdef USE_RADIO_FREQUENCY
#include "esphome/components/radio_frequency/radio_frequency.h"
#endif
namespace esphome::ir_rf_proxy {
#ifdef USE_IR_RF
/// IrRfProxy - Infrared platform implementation using remote_transmitter/receiver as backend
class IrRfProxy : public infrared::Infrared {
public:
@@ -35,36 +26,8 @@ class IrRfProxy : public infrared::Infrared {
void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); }
protected:
void control(const infrared::InfraredCall &call) override;
// RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF
uint32_t frequency_khz_{0};
};
#endif // USE_IR_RF
#ifdef USE_RADIO_FREQUENCY
/// RfProxy - Radio Frequency platform implementation using remote_transmitter/receiver as backend
class RfProxy : public radio_frequency::RadioFrequency {
public:
RfProxy() = default;
void setup() override;
void dump_config() override;
/// Set the remote transmitter component
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
/// Set the remote receiver component
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
/// Set the fixed carrier frequency in Hz (metadata: advertised via traits, does not tune hardware)
void set_frequency_hz(uint32_t freq_hz) { this->traits_.set_fixed_frequency_hz(freq_hz); }
protected:
void control(const radio_frequency::RadioFrequencyCall &call) override;
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
remote_base::RemoteReceiverBase *receiver_{nullptr};
};
#endif // USE_RADIO_FREQUENCY
} // namespace esphome::ir_rf_proxy
@@ -1,68 +0,0 @@
"""Radio Frequency platform implementation using remote_base (remote_transmitter/receiver)."""
import esphome.codegen as cg
from esphome.components import radio_frequency, remote_receiver, remote_transmitter
import esphome.config_validation as cv
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
import esphome.final_validate as fv
from esphome.types import ConfigType
from . import CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID, ir_rf_proxy_ns
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["radio_frequency"]
RfProxy = ir_rf_proxy_ns.class_("RfProxy", radio_frequency.RadioFrequency)
CONFIG_SCHEMA = cv.All(
radio_frequency.radio_frequency_schema(RfProxy).extend(
{
cv.Optional(CONF_FREQUENCY): cv.frequency,
cv.Optional(CONF_REMOTE_RECEIVER_ID): cv.use_id(
remote_receiver.RemoteReceiverComponent
),
cv.Optional(CONF_REMOTE_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
}
),
cv.has_exactly_one_key(CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID),
)
def _final_validate(config: ConfigType) -> None:
"""Validate that RF transmitters have carrier duty set to 100%."""
if CONF_REMOTE_TRANSMITTER_ID not in config:
return
transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID]
full_config = fv.full_config.get()
transmitter_path = full_config.get_path_for_id(transmitter_id)[:-1]
transmitter_config = full_config.get_config_for_path(transmitter_path)
duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT)
if duty_percent is not None and duty_percent != 100:
raise cv.Invalid(
f"Transmitter '{transmitter_id}' must have '{CONF_CARRIER_DUTY_PERCENT}' "
"set to 100% for RF transmission. Dedicated RF hardware handles modulation; "
"applying a carrier duty cycle would corrupt the signal"
)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
"""Code generation for remote_base radio frequency platform."""
var = await radio_frequency.new_radio_frequency(config)
if CONF_FREQUENCY in config:
cg.add(var.set_frequency_hz(int(config[CONF_FREQUENCY])))
if CONF_REMOTE_TRANSMITTER_ID in config:
transmitter = await cg.get_variable(config[CONF_REMOTE_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter))
if CONF_REMOTE_RECEIVER_ID in config:
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
cg.add(var.set_receiver(receiver))
+2 -3
View File
@@ -39,8 +39,7 @@ bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
}
JsonDocument parse_json(const uint8_t *data, size_t len) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape) false positives with
// ArduinoJson
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (data == nullptr || len == 0) {
ESP_LOGE(TAG, "No data to parse");
return JsonObject(); // return unbound object
@@ -64,7 +63,7 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
}
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
return JsonObject(); // return unbound object
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape)
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
SerializationBuffer<> JsonBuilder::serialize() {
-4
View File
@@ -58,7 +58,6 @@ COMPONENT_RTL87XX = "rtl87xx"
FAMILY_BK7231N = "BK7231N"
FAMILY_BK7231Q = "BK7231Q"
FAMILY_BK7231T = "BK7231T"
FAMILY_BK7238 = "BK7238"
FAMILY_BK7251 = "BK7251"
FAMILY_LN882H = "LN882H"
FAMILY_RTL8710B = "RTL8710B"
@@ -67,7 +66,6 @@ FAMILIES = [
FAMILY_BK7231N,
FAMILY_BK7231Q,
FAMILY_BK7231T,
FAMILY_BK7238,
FAMILY_BK7251,
FAMILY_LN882H,
FAMILY_RTL8710B,
@@ -77,7 +75,6 @@ FAMILY_FRIENDLY = {
FAMILY_BK7231N: "BK7231N",
FAMILY_BK7231Q: "BK7231Q",
FAMILY_BK7231T: "BK7231T",
FAMILY_BK7238: "BK7238",
FAMILY_BK7251: "BK7251",
FAMILY_LN882H: "LN882H",
FAMILY_RTL8710B: "RTL8710B",
@@ -87,7 +84,6 @@ FAMILY_COMPONENT = {
FAMILY_BK7231N: COMPONENT_BK72XX,
FAMILY_BK7231Q: COMPONENT_BK72XX,
FAMILY_BK7231T: COMPONENT_BK72XX,
FAMILY_BK7238: COMPONENT_BK72XX,
FAMILY_BK7251: COMPONENT_BK72XX,
FAMILY_LN882H: COMPONENT_LN882X,
FAMILY_RTL8710B: COMPONENT_RTL87XX,
+27 -1
View File
@@ -3,6 +3,7 @@
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/time_64.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
@@ -14,7 +15,32 @@ void loop();
namespace esphome {
// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h.
void HOT yield() { ::yield(); }
// Inline the tick read so esphome::millis() matches MillisInternal::get()'s fast
// path instead of going through the Arduino core's out-of-line ::millis() wrapper.
//
// RTL87xx / LN882x (1 kHz): xTaskGetTickCount() is already ms. IRAM_ATTR + ISR
// dispatch are needed because ISR handlers (e.g. rotary_encoder) call millis().
//
// BK72xx (500 Hz): ticks * portTICK_PERIOD_MS (== 2). IRAM_ATTR and ISR dispatch
// are both unnecessary — the SDK masks FIQ + IRQ during flash writes (see hal.h),
// so no ISR runs while flash is stalled.
#if defined(USE_RTL87XX) || defined(USE_LN882X)
uint32_t IRAM_ATTR HOT millis() {
static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick");
return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount();
}
#elif defined(USE_BK72XX)
uint32_t HOT millis() {
static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick");
return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
#else
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
#endif
uint64_t millis_64() { return Millis64Impl::compute(millis()); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void HOT delay(uint32_t ms) { ::delay(ms); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
void arch_init() {
+2 -6
View File
@@ -40,11 +40,7 @@ from esphome.const import (
CONF_WHITE,
)
from esphome.core import CORE, ID, CoroPriority, HexInt, Lambda, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -409,7 +405,7 @@ async def setup_light_core_(light_var, config, output_var):
async def register_light(output_var, config):
light_var = cg.new_Pvariable(config[CONF_ID], output_var)
queue_entity_register("light", config)
cg.add(cg.App.register_light(light_var))
CORE.register_platform_component("light", light_var)
await cg.register_component(light_var, config)
await setup_light_core_(light_var, config, output_var)
+2 -6
View File
@@ -13,11 +13,7 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@esphome/core"]
@@ -116,7 +112,7 @@ async def _setup_lock_core(var, config):
async def register_lock(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
queue_entity_register("lock", config)
cg.add(cg.App.register_lock(var))
CORE.register_platform_component("lock", var)
await _setup_lock_core(var, config)
@@ -4,25 +4,15 @@ from esphome.components.binary_sensor import (
new_binary_sensor,
)
import esphome.config_validation as cv
from esphome.const import CONF_STATE
from ..defines import CONF_WIDGET, LV_OBJ_FLAG, LvConstant
from ..lvcode import EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext, lvgl_static
from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t
from ..defines import CONF_WIDGET
from ..lvcode import EVENT_ARG, LambdaContext, LvContext, lvgl_static
from ..types import LV_EVENT, lv_pseudo_button_t
from ..widgets import Widget, get_widgets, wait_for_widgets
STATE_PRESSED = "PRESSED"
STATE_CHECKED = "CHECKED"
BS_STATE = LvConstant(
"LV_STATE_",
STATE_PRESSED,
STATE_CHECKED,
)
CONFIG_SCHEMA = binary_sensor_schema(BinarySensor).extend(
{
cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t),
cv.Optional(CONF_STATE, default=STATE_PRESSED): BS_STATE.one_of,
}
)
@@ -32,23 +22,16 @@ async def to_code(config):
widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0]
assert isinstance(widget, Widget)
state = await BS_STATE.process(config[CONF_STATE])
await wait_for_widgets()
is_pressed = str(state) == str(LV_STATE.PRESSED)
test_expr = widget.is_pressed() if is_pressed else widget.is_checked()
async with LambdaContext(EVENT_ARG) as test_ctx:
test_ctx.add(sensor.publish_state(test_expr))
async with LambdaContext(EVENT_ARG) as pressed_ctx:
pressed_ctx.add(sensor.publish_state(widget.is_pressed()))
async with LvContext() as ctx:
ctx.add(sensor.publish_initial_state(test_expr))
if is_pressed:
events = [LV_EVENT.PRESSED, LV_EVENT.RELEASED]
widget.add_flag(LV_OBJ_FLAG.CLICKABLE)
else:
events = [LV_EVENT.VALUE_CHANGED, UPDATE_EVENT]
ctx.add(sensor.publish_initial_state(widget.is_pressed()))
ctx.add(
lvgl_static.add_event_cb(
widget.obj,
await test_ctx.get_lambda(),
*events,
await pressed_ctx.get_lambda(),
LV_EVENT.PRESSED,
LV_EVENT.RELEASED,
)
)
+1 -5
View File
@@ -1,5 +1,3 @@
from operator import itemgetter
from esphome import config_validation as cv
import esphome.codegen as cg
from esphome.const import (
@@ -13,7 +11,6 @@ from esphome.core import ID
from esphome.cpp_generator import MockObj
from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning
from .helpers import add_lv_use
from .lv_validation import lv_color, lv_percentage, opacity
from .lvcode import lv
from .types import lv_color_t, lv_gradient_t, lv_opa_t
@@ -53,7 +50,6 @@ GRADIENT_SCHEMA = cv.ensure_list(
async def gradients_to_code(config):
add_lv_use("gradient")
max_stops = 2
if any(CONF_DITHER in x for x in config.get(CONF_GRADIENTS, ())):
add_warning(
@@ -62,7 +58,7 @@ async def gradients_to_code(config):
for gradient in config.get(CONF_GRADIENTS, ()):
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
idbase = gradient[CONF_ID].id
stops = sorted(gradient[CONF_STOPS], key=itemgetter(CONF_POSITION))
stops = gradient[CONF_STOPS]
max_stops = max(max_stops, len(stops))
if gradient[CONF_DIRECTION].startswith("VER"):
lv.grad_vertical_init(var)
+22 -97
View File
@@ -1,4 +1,3 @@
import math
import re
import textwrap
@@ -86,22 +85,6 @@ def grid_free_space(value):
grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space)
def grid_dimension(value):
"""
Validator for a grid `rows` or `columns` value.
Accepts either a positive integer (interpreted as that many cells of equal
`LV_GRID_FR(1)` size) or a non-empty list of grid specs.
"""
if isinstance(value, int):
value = cv.int_range(min=1)(value)
return ["LV_GRID_FR(1)"] * value
result = cv.Schema([grid_spec])(value)
if not result:
raise cv.Invalid("Grid dimension list must contain at least one entry")
return result
GRID_CELL_SCHEMA = {
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
@@ -201,16 +184,7 @@ class DirectionalLayout(FlexLayout):
class GridLayout(Layout):
# Match shorthand grid layout strings: "NxM", "Nx" or "xM".
# At least one of the two numbers must be present; this is enforced after matching.
_GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)?\s*x\s*(\d+)?\s*$")
@staticmethod
def _match_shorthand(layout):
match = GridLayout._GRID_LAYOUT_REGEX.match(layout)
if match is None or (match.group(1) is None and match.group(2) is None):
return None
return match
_GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)\s*x\s*(\d+)\s*$")
def get_type(self):
return TYPE_GRID
@@ -218,7 +192,7 @@ class GridLayout(Layout):
def get_layout_schemas(self, config: dict) -> tuple:
layout = config.get(CONF_LAYOUT)
if isinstance(layout, str):
if GridLayout._match_shorthand(layout):
if GridLayout._GRID_LAYOUT_REGEX.match(layout):
return (
cv.string,
{
@@ -239,107 +213,59 @@ class GridLayout(Layout):
if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_GRID:
return None, {}
x_default = (
"center" if isinstance(layout.get(CONF_GRID_ROWS), int) else cv.UNDEFINED
)
y_default = (
"center" if isinstance(layout.get(CONF_GRID_COLUMNS), int) else cv.UNDEFINED
)
x_align = layout.get(CONF_GRID_CELL_X_ALIGN, x_default)
y_align = layout.get(CONF_GRID_CELL_Y_ALIGN, y_default)
return (
{
cv.Required(CONF_TYPE): cv.one_of(TYPE_GRID, lower=True),
cv.Optional(CONF_GRID_ROWS): grid_dimension,
cv.Optional(CONF_GRID_COLUMNS): grid_dimension,
cv.Required(CONF_GRID_ROWS): [grid_spec],
cv.Required(CONF_GRID_COLUMNS): [grid_spec],
cv.Optional(CONF_GRID_COLUMN_ALIGN): grid_alignments,
cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments,
cv.Optional(CONF_PAD_ROW): padding,
cv.Optional(CONF_PAD_COLUMN): padding,
cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean,
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
},
{
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1),
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1),
cv.Optional(CONF_GRID_CELL_X_ALIGN, default=x_align): grid_alignments,
cv.Optional(CONF_GRID_CELL_Y_ALIGN, default=y_align): grid_alignments,
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
},
)
def validate(self, config: dict):
"""
Validate the grid layout.
The `layout:` key may be a dictionary with `rows` and/or `columns` keys, or a
shorthand string in the format "<rows>x<columns>", "<rows>x" or "x<columns>".
Either dimension may be omitted, in which case it will be calculated from the
other dimension and the number of configured widgets.
The `layout:` key may be a dictionary with `rows` and `columns` keys, or a string in the format "rows x columns".
Either all cells must have a row and column,
or none, in which case the grid layout is auto-generated.
:param config:
:return: The config updated with auto-generated values
"""
layout = config.get(CONF_LAYOUT)
widgets = config.get(CONF_WIDGETS, [])
num_widgets = len(widgets)
if isinstance(layout, str):
# Shorthand string: "<rows>x<columns>", "<rows>x" or "x<columns>".
# Each dimension defaults to LV_GRID_FR(1). A missing dimension is
# calculated from the other dimension and the number of widgets.
# If the layout is a string, assume it is in the format "rows x columns", implying
# a grid layout with the specified number of rows and columns each with CONTENT sizing.
layout = layout.strip()
match = GridLayout._match_shorthand(layout)
if not match:
match = GridLayout._GRID_LAYOUT_REGEX.match(layout)
if match:
rows = int(match.group(1))
cols = int(match.group(2))
layout = {
CONF_TYPE: TYPE_GRID,
CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows,
CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols,
}
config[CONF_LAYOUT] = layout
else:
raise cv.Invalid(
f"Invalid grid layout format: {layout!r}, expected "
"'<rows>x<columns>', '<rows>x' or 'x<columns>'",
f"Invalid grid layout format: {config}, expected 'rows x columns'",
[CONF_LAYOUT],
)
rows_int = int(match.group(1)) if match.group(1) is not None else None
cols_int = int(match.group(2)) if match.group(2) is not None else None
for label, val in (("row", rows_int), ("column", cols_int)):
if val is not None and val < 1:
raise cv.Invalid(
f"Invalid grid layout {layout!r}: {label} count must be "
"at least 1",
[CONF_LAYOUT],
)
if rows_int is not None and cols_int is not None:
rows = rows_int
cols = cols_int
elif rows_int is not None:
rows = rows_int
cols = max(1, math.ceil(num_widgets / rows)) if num_widgets else 1
else:
cols = cols_int
rows = max(1, math.ceil(num_widgets / cols)) if num_widgets else 1
layout = {
CONF_TYPE: TYPE_GRID,
CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows,
CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols,
}
config[CONF_LAYOUT] = layout
# should be guaranteed to be a dict at this point
assert isinstance(layout, dict)
assert layout.get(CONF_TYPE).lower() == TYPE_GRID
rows_list = layout.get(CONF_GRID_ROWS)
cols_list = layout.get(CONF_GRID_COLUMNS)
if rows_list is None and cols_list is None:
raise cv.Invalid(
"Grid layout requires at least one of 'rows' or 'columns' to be "
"specified",
[CONF_LAYOUT],
)
if rows_list is None:
cols = len(cols_list)
rows = max(1, math.ceil(num_widgets / cols)) if num_widgets else 1
layout[CONF_GRID_ROWS] = ["LV_GRID_FR(1)"] * rows
elif cols_list is None:
rows = len(rows_list)
cols = max(1, math.ceil(num_widgets / rows)) if num_widgets else 1
layout[CONF_GRID_COLUMNS] = ["LV_GRID_FR(1)"] * cols
allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False)
rows = len(layout[CONF_GRID_ROWS])
columns = len(layout[CONF_GRID_COLUMNS])
@@ -453,8 +379,7 @@ def append_layout_schema(schema, config: dict):
textwrap.dedent(
"""
Invalid 'layout' value
layout choices are 'horizontal', 'vertical',
'<rows>x<cols>', '<rows>x', 'x<cols>',
layout choices are 'horizontal', 'vertical', '<rows>x<cols>',
or a dictionary with a 'type' key
"""
),
-26
View File
@@ -864,32 +864,6 @@ void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_en
}
#endif // USE_LVGL_SCALE
#ifdef USE_LVGL_GRADIENT
/**
*
* @param dsc The gradient descriptor containing the color stops
* @param pos The current position to calculate the color for
* @return The color for the given position
*/
lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos) {
if (dsc->stops_count == 0)
return lv_color_black();
if (dsc->stops_count == 1 || pos <= dsc->stops[0].frac)
return dsc->stops[0].color;
if (pos >= dsc->stops[dsc->stops_count - 1].frac)
return dsc->stops[dsc->stops_count - 1].color;
int i = 1;
while (i < dsc->stops_count && dsc->stops[i].frac < pos)
i++;
auto *stop1 = &dsc->stops[i - 1];
auto *stop2 = &dsc->stops[i];
int32_t range = stop2->frac - stop1->frac;
int32_t offset = pos - stop1->frac;
return lv_color_mix(stop2->color, stop1->color, range == 0 ? 0 : (offset * 255) / range);
}
#endif
static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) {
LV_TRACE_OBJ_CREATE("begin");
LV_UNUSED(class_p);
-10
View File
@@ -115,16 +115,6 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value);
#endif
#ifdef USE_LVGL_GRADIENT
/**
*
* @param dsc The gradient descriptor containing the color stops
* @param pos The current position to calculate the color for
* @return The color for the given position
*/
lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos);
#endif
// Parent class for things that wrap an LVGL object
class LvCompound {
public:
+32 -81
View File
@@ -1,27 +1,18 @@
from collections.abc import Callable
import difflib
import esphome.codegen as cg
from esphome.components.const import KEY_METADATA
import esphome.config_validation as cv
from esphome.const import CONF_FROM, CONF_ID, CONF_TO
from esphome.core import CORE, ID
from esphome.cpp_generator import (
MockObj,
MockObjClass,
VariableDeclarationExpression,
add_global,
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj, VariableDeclarationExpression, add_global
from esphome.loader import get_component
CODEOWNERS = ["@clydebarrow"]
MULTI_CONF = True
DOMAIN = "mapping"
mapping_ns = cg.esphome_ns.namespace("mapping")
mapping_class = mapping_ns.class_("Mapping")
CONF_DEFAULT_VALUE = "default_value"
CONF_ENTRIES = "entries"
CONF_CLASS = "class"
@@ -31,18 +22,11 @@ class IndexType:
Represents a type of index in a map.
"""
def __init__(
self, validator: Callable, data_type: MockObj, conversion: Callable = None
) -> None:
def __init__(self, validator, data_type, conversion):
self.validator = validator
self.data_type = data_type
self.conversion = conversion
async def convert_value(self, value):
if self.conversion:
return self.conversion(value)
return await cg.get_variable(value)
INDEX_TYPES = {
"int": IndexType(cv.int_, cg.int_, int),
@@ -54,12 +38,6 @@ INDEX_TYPES = {
}
class MappingMetaData:
def __init__(self, from_: IndexType, to_: IndexType) -> None:
self.from_ = from_
self.to_ = to_
def to_schema(value):
"""
Generate a schema for the 'to' field of a map. This can be either one of the index types or a class name.
@@ -82,7 +60,7 @@ BASE_SCHEMA = cv.Schema(
)
def get_object_type(to_) -> MockObjClass | None:
def get_object_type(to_):
"""
Get the object type from a string. Possible formats:
xxx The name of a component which defines INSTANCE_TYPE
@@ -103,60 +81,25 @@ def get_object_type(to_) -> MockObjClass | None:
return None
def get_all_mapping_metadata() -> dict[str, MappingMetaData]:
"""Get all mapping metadata."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
def get_mapping_metadata(mapping_id: str) -> MappingMetaData:
"""Get mapping metadata by ID for use by other components."""
return get_all_mapping_metadata()[mapping_id]
def add_metadata(
mapping_id: ID,
from_: IndexType,
to_: IndexType,
) -> None:
get_all_mapping_metadata()[mapping_id.id] = MappingMetaData(from_, to_)
def map_schema(config):
config = BASE_SCHEMA(config)
if CONF_ENTRIES not in config or not isinstance(config[CONF_ENTRIES], dict):
raise cv.Invalid("an entries dictionary is required for a mapping")
raise cv.Invalid("an entries list is required for a map")
entries = config[CONF_ENTRIES]
if len(entries) == 0:
raise cv.Invalid("A mapping must have at least one entry")
raise cv.Invalid("Map must have at least one entry")
to_ = config[CONF_TO]
if to_ in INDEX_TYPES:
value_type = INDEX_TYPES[to_]
value_type = INDEX_TYPES[to_].validator
else:
object_type = get_object_type(to_)
if object_type is None:
value_type = get_object_type(to_)
if value_type is None:
matches = difflib.get_close_matches(to_, CORE.id_classes)
raise cv.Invalid(
f"No known mappable class name matches '{to_}'; did you mean one of {', '.join(matches)}?"
)
validator = cv.use_id(object_type)
value_type = IndexType(validator, object_type)
config[CONF_ENTRIES] = {k: value_type.validator(v) for k, v in entries.items()}
if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None:
config[CONF_DEFAULT_VALUE] = value_type.validator(default_value)
unexpected_keys = config.keys() - {
CONF_ENTRIES,
CONF_TO,
CONF_FROM,
CONF_ID,
CONF_DEFAULT_VALUE,
}
if unexpected_keys:
errors = [
cv.Invalid(f"Unexpected key '{k}'", path=[k]) for k in unexpected_keys
]
raise cv.MultipleInvalid(errors)
add_metadata(config[CONF_ID], INDEX_TYPES[config[CONF_FROM]], value_type)
value_type = cv.use_id(value_type)
config[CONF_ENTRIES] = {k: value_type(v) for k, v in entries.items()}
return config
@@ -164,19 +107,29 @@ CONFIG_SCHEMA = map_schema
async def to_code(config):
entries = config[CONF_ENTRIES]
from_ = config[CONF_FROM]
to_ = config[CONF_TO]
index_conversion = INDEX_TYPES[from_].conversion
index_type = INDEX_TYPES[from_].data_type
if to_ in INDEX_TYPES:
value_conversion = INDEX_TYPES[to_].conversion
value_type = INDEX_TYPES[to_].data_type
entries = {
index_conversion(key): value_conversion(value)
for key, value in entries.items()
}
else:
entries = {
index_conversion(key): await cg.get_variable(value)
for key, value in entries.items()
}
value_type = get_object_type(to_)
if list(entries.values())[0].op != ".":
value_type = value_type.operator("ptr")
varid = config[CONF_ID]
metadata = get_mapping_metadata(varid.id)
entries = {
metadata.from_.conversion(key): await metadata.to_.convert_value(value)
for key, value in config[CONF_ENTRIES].items()
}
value_type = metadata.to_.data_type
# entries guaranteed to be non-empty here.
value_0 = list(entries.values())[0]
if isinstance(value_0, MockObj) and value_0.op != ".":
value_type = value_type.operator("ptr")
varid.type = mapping_class.template(
metadata.from_.data_type,
index_type,
value_type,
)
var = MockObj(varid, ".")
@@ -186,6 +139,4 @@ async def to_code(config):
for key, value in entries.items():
cg.add(var.set(key, value))
if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None:
cg.add(var.set_default_value(await metadata.to_.convert_value(default_value)))
return var
-9
View File
@@ -40,9 +40,6 @@ template<typename K, typename V> class Mapping {
if (it != this->map_.end()) {
return V{it->second};
}
if (this->default_value_.has_value()) {
return this->default_value_.value();
}
if constexpr (std::is_pointer_v<K>) {
esph_log_e(TAG, "Key '%p' not found in mapping", key);
} else if constexpr (std::is_same_v<K, std::string>) {
@@ -72,17 +69,11 @@ template<typename K, typename V> class Mapping {
if (it != this->map_.end()) {
return it->second.c_str(); // safe since value remains in map
}
if (this->default_value_.has_value()) {
return this->default_value_.value();
}
return "";
}
void set_default_value(const V &default_value) { this->default_value_ = default_value; }
protected:
std::map<key_t, value_t, std::less<key_t>, RAMAllocator<std::pair<key_t, value_t>>> map_;
std::optional<V> default_value_{};
};
} // namespace esphome::mapping
-39
View File
@@ -14,7 +14,6 @@ from esphome.const import (
from esphome.core import CORE, Lambda, coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.cpp_generator import LambdaExpression
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
@@ -62,28 +61,6 @@ def _consume_mdns_sockets(config: ConfigType) -> ConfigType:
return config
def _require_network_interface(config: ConfigType) -> ConfigType:
"""Require a network interface for mDNS on Arduino/LEAmDNS platforms.
On ESP8266 and RP2040 the C++ implementation needs at least one IP state
listener (WiFi on ESP8266; WiFi or Ethernet on RP2040) to arm its polling
window. Reject at config time rather than silently producing a component
that never initializes.
"""
if config.get(CONF_DISABLED) or not (CORE.is_esp8266 or CORE.is_rp2040):
return config
full_config = fv.full_config.get()
has_wifi = "wifi" in full_config
has_ethernet = CORE.is_rp2040 and "ethernet" in full_config
if not (has_wifi or has_ethernet):
options = "'wifi'" if CORE.is_esp8266 else "'wifi' or 'ethernet'"
raise cv.Invalid(
"mdns on this platform requires a network interface — "
f"add a {options} component to your configuration."
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -97,9 +74,6 @@ CONFIG_SCHEMA = cv.All(
)
FINAL_VALIDATE_SCHEMA = _require_network_interface
def mdns_txt_record(key: str, value: str) -> cg.RawExpression:
"""Create a mDNS TXT record.
@@ -195,19 +169,6 @@ async def to_code(config):
elif CORE.is_rp2040:
cg.add_library("LEAmDNS", None)
# Subscribe to the network IP state listener(s) so MDNS.update() is only
# scheduled during the probe+announce phase. Same on_ip_state() override
# serves both WiFi and Ethernet (signatures match).
if CORE.is_esp8266 or CORE.is_rp2040:
if "wifi" in CORE.config:
from esphome.components import wifi
wifi.request_wifi_ip_state_listener()
if CORE.is_rp2040 and "ethernet" in CORE.config:
from esphome.components import ethernet
ethernet.request_ethernet_ip_state_listener()
if CORE.is_esp32:
add_idf_component(name="espressif/mdns", ref="1.11.0")
+24 -57
View File
@@ -5,22 +5,6 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
// On ESP8266 and RP2040 the scheduler-backed MDNS.update() polling window is armed by
// IP state listener events on whichever network interface is configured.
#if (defined(USE_ESP8266) || defined(USE_RP2040)) && \
((defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS)) || \
(defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS)))
#include "esphome/components/network/ip_address.h"
#define USE_MDNS_EVENT_DRIVEN_POLLING
#if defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS)
#include "esphome/components/wifi/wifi_component.h"
#define USE_MDNS_WIFI_LISTENER
#endif
#if defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS)
#include "esphome/components/ethernet/ethernet_component.h"
#define USE_MDNS_ETHERNET_LISTENER
#endif
#endif
namespace esphome::mdns {
@@ -56,40 +40,33 @@ struct MDNSService {
FixedVector<MDNSTXTRecord> txt_records;
};
class MDNSComponent final : public Component
#ifdef USE_MDNS_WIFI_LISTENER
,
public wifi::WiFiIPStateListener
#endif
#ifdef USE_MDNS_ETHERNET_LISTENER
,
public ethernet::EthernetIPStateListener
#endif
{
class MDNSComponent final : public Component {
public:
void setup() override;
void dump_config() override;
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
// LEAmDNS has meaningful work only during the probe+announce phase (3×250ms probes +
// 8×1000ms announces, ~9s). Afterwards every internal timer is resetToNeverExpires()
// and update() becomes pure overhead. We arm a bounded polling window from IP state
// listener events so update() runs only during that phase.
// Polling interval for MDNS.update() on platforms that require it (ESP8266, RP2040).
//
// On these platforms, MDNS.update() calls _process(true) which only manages timer-driven
// state machines (probe/announce timeouts and service query cache TTLs). Incoming mDNS
// packets are handled independently via the lwIP onRx UDP callback and are NOT affected
// by how often update() is called.
//
// The shortest internal timer is the 250ms probe interval (RFC 6762 Section 8.1).
// Announcement intervals are 1000ms and cache TTL checks are on the order of seconds
// to minutes. A 50ms polling interval provides sufficient resolution for all timers
// while completely removing mDNS from the per-iteration loop list.
//
// In steady state (after the ~8 second boot probe/announce phase completes), update()
// checks timers that are set to never expire, making every call pure overhead.
//
// Tasmota uses a 50ms main loop cycle with mDNS working correctly, confirming this
// interval is safe in production.
//
// By using set_interval() instead of overriding loop(), the component is excluded from
// the main loop list via has_overridden_loop(), eliminating all per-iteration overhead
// including virtual dispatch.
static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50;
// Must exceed LEAmDNS's longest restart-to-announce-complete path:
// MDNS_PROBE_DELAY (250ms) × MDNS_PROBE_COUNT (3) = 750ms probing
// + MDNS_ANNOUNCE_DELAY (1000ms) × MDNS_ANNOUNCE_COUNT (8) = 8000ms announcing
// + rand() % MDNS_PROBE_DELAY jitter on first probe (0250ms)
// + debounced schedule_function() hop when statusChangeCB fires on ESP8266
// ≈ 9s nominal. 15s gives ~6s margin to absorb main-loop blocking (long
// component setup, WiFi scan, flash writes) that could stretch the deadlines
// between our polls. If LEAmDNS ever extends its phase (upstream library
// update) this constant needs to grow. Constants defined in LEAmDNS_Priv.h
// (ESP8266 core 3.1.2 / arduino-pico 5.5.1).
static constexpr uint32_t MDNS_POLL_WINDOW_MS = 15000;
static constexpr uint32_t MDNS_POLL_ID = 0;
static constexpr uint32_t MDNS_POLL_STOP_ID = 1;
#endif
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
#ifdef USE_MDNS_EXTRA_SERVICES
@@ -110,17 +87,7 @@ class MDNSComponent final : public Component
}
#endif
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
#endif
protected:
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
/// Arm a fresh MDNS_POLL_WINDOW_MS polling window. Idempotent — re-arming replaces
/// the previous window via the scheduler's atomic cancel-and-add on matching IDs.
void start_polling_window_();
#endif
/// Helper to set up services and MAC buffers, then call platform-specific registration
using PlatformRegisterFn = void (*)(MDNSComponent *, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &);
@@ -163,8 +130,8 @@ class MDNSComponent final : public Component
#ifdef USE_MDNS_STORE_SERVICES
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
#endif
#if defined(USE_RP2040) && defined(USE_MDNS_EVENT_DRIVEN_POLLING)
// RP2040 defers MDNS.begin() until the first IP-up event; this tracks that.
#ifdef USE_RP2040
bool was_connected_{false};
bool initialized_{false};
#endif
void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf);
+5 -28
View File
@@ -8,8 +8,6 @@
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "mdns_component.h"
// wifi_component.h is pulled in transitively by mdns_component.h when
// USE_MDNS_WIFI_LISTENER is defined.
namespace esphome::mdns {
@@ -38,36 +36,15 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
}
}
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void MDNSComponent::start_polling_window_() {
// uint32_t-ID set_interval/set_timeout already does atomic cancel-and-add.
this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); });
}
#endif
void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_esp8266);
#ifdef USE_MDNS_WIFI_LISTENER
// LEAmDNS's own LwipIntf::statusChangeCB drives _restart() on netif changes; we just
// arm the window around the initial probe/announce and each reconnect. Unconditional
// here is safe: setup_priority::AFTER_CONNECTION guarantees the network is up.
wifi::global_wifi_component->add_ip_state_listener(this);
this->start_polling_window_();
#endif
// Schedule MDNS.update() via set_interval() instead of overriding loop().
// This removes the component from the per-iteration loop list entirely,
// eliminating virtual dispatch overhead on every main loop cycle.
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
}
#ifdef USE_MDNS_WIFI_LISTENER
void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &,
const network::IPAddress &) {
// IP listener only fires on acquisition (not loss), so any notification is a fresh
// IP worth re-arming for. start_polling_window_() is idempotent.
if (ips[0].is_set()) {
this->start_polling_window_();
}
}
#endif
void MDNSComponent::on_shutdown() {
MDNS.close();
delay(10);
+29 -51
View File
@@ -6,10 +6,9 @@
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "mdns_component.h"
// wifi_component.h / ethernet_component.h are pulled in transitively by
// mdns_component.h when their respective listener defines are active.
// Arduino-Pico's PolledTimeout.h (pulled in by ESP8266mDNS.h) redefines IRAM_ATTR to empty.
// Save and restore our definition around the include to avoid a redefinition warning.
#pragma push_macro("IRAM_ATTR")
#undef IRAM_ATTR
#include <ESP8266mDNS.h>
@@ -21,7 +20,10 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
MDNS.begin(App.get_name().c_str());
for (const auto &service : services) {
// ESP8266mDNS always adds the leading underscore itself, so strip it here.
// Strip the leading underscore from the proto and service_type. While it is
// part of the wire protocol to have an underscore, and for example ESP-IDF
// expects the underscore to be there, the ESP8266 implementation always adds
// the underscore itself.
auto *proto = MDNS_STR_ARG(service.proto);
while (*proto == '_') {
proto++;
@@ -38,59 +40,35 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
}
}
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void MDNSComponent::start_polling_window_() {
// uint32_t-ID set_interval/set_timeout already does atomic cancel-and-add.
this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); });
}
#endif
void MDNSComponent::setup() {
// arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses
// on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our
// own IP state listener. Both WiFi and Ethernet have the same listener signature —
// one on_ip_state() override serves both.
#ifdef USE_MDNS_WIFI_LISTENER
wifi::global_wifi_component->add_ip_state_listener(this);
// AFTER_CONNECTION priority means the network may already be up; the listener only
// fires on subsequent changes, so seed the current state.
{
const auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses();
if (ips[0].is_set()) {
this->on_ip_state(ips, wifi::global_wifi_component->get_dns_address(0),
wifi::global_wifi_component->get_dns_address(1));
// RP2040's LEAmDNS library registers a LwipIntf::stateUpCB() callback to restart
// mDNS when the network interface reconnects. However, stateUpCB() is stubbed out
// in arduino-pico's LwipIntfCB.cpp because the original ESP8266 implementation used
// schedule_function() which doesn't exist in arduino-pico, and the callback can't
// safely run directly since netif status callbacks fire from IRQ context
// (PICO_CYW43_ARCH_THREADSAFE_BACKGROUND) while _restart() allocates UDP sockets.
//
// Workaround: defer MDNS.begin() and service registration until the network is
// connected (has an IP), then call notifyAPChange() on subsequent reconnects to
// restart mDNS probing and announcing — all from main loop context so it's
// thread-safe.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, [this]() {
bool connected = network::is_connected();
if (connected && !this->was_connected_) {
if (!this->initialized_) {
this->setup_buffers_and_register_(register_rp2040);
this->initialized_ = true;
} else {
MDNS.notifyAPChange();
}
}
}
#endif
#ifdef USE_MDNS_ETHERNET_LISTENER
ethernet::global_eth_component->add_ip_state_listener(this);
if (ethernet::global_eth_component->is_connected()) {
const auto ips = ethernet::global_eth_component->get_ip_addresses();
if (ips[0].is_set()) {
this->on_ip_state(ips, network::IPAddress{}, network::IPAddress{});
this->was_connected_ = connected;
if (this->initialized_) {
MDNS.update();
}
}
#endif
});
}
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &,
const network::IPAddress &) {
// Listener only fires on IP acquisition (not loss); every event is a fresh IP.
if (!ips[0].is_set()) {
return;
}
if (!this->initialized_) {
this->setup_buffers_and_register_(register_rp2040);
this->initialized_ = true;
} else {
MDNS.notifyAPChange();
}
this->start_polling_window_();
}
#endif
void MDNSComponent::on_shutdown() {
MDNS.close();
delay(40);
+1 -2
View File
@@ -21,7 +21,6 @@ from esphome.core import CORE
from esphome.core.entity_helpers import (
entity_duplicate_validator,
inherit_property_from,
queue_entity_register,
setup_entity,
)
from esphome.coroutine import CoroPriority, coroutine_with_priority
@@ -263,7 +262,7 @@ async def setup_media_player_core_(var, config):
async def register_media_player(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
queue_entity_register("media_player", config)
cg.add(cg.App.register_media_player(var))
CORE.register_platform_component("media_player", var)
await setup_media_player_core_(var, config)
@@ -352,7 +352,7 @@ void MitsubishiCN105::set_target_temperature(float target_temperature) {
ESP_LOGD(TAG, "Setting temperature out-of-range: %.1f", target_temperature);
return;
}
this->status_.target_temperature = target_temperature;
this->status_.target_temperature = std::round(target_temperature);
this->pending_updates_.set(UpdateFlag::TEMPERATURE);
}
@@ -387,9 +387,9 @@ void MitsubishiCN105::apply_settings_() {
if (this->pending_updates_.has(UpdateFlag::TEMPERATURE)) {
payload[1] |= 0x04;
if (this->use_temperature_encoding_b_) {
payload[14] = static_cast<uint8_t>(std::round(this->status_.target_temperature * 2.0f) + 128);
payload[14] = static_cast<uint8_t>(this->status_.target_temperature * 2.0f + 128.0f);
} else {
payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - std::round(this->status_.target_temperature));
payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - this->status_.target_temperature);
}
}
@@ -3,8 +3,11 @@ import binascii
from esphome import automation
import esphome.codegen as cg
from esphome.components import modbus
from esphome.components.const import CONF_ENABLED
from esphome.components.modbus.helpers import (
CPP_TYPE_REGISTER_MAP,
MODBUS_REGISTER_TYPE,
SENSOR_VALUE_TYPE,
TYPE_REGISTER_MAP,
ModbusRegisterType,
)
@@ -26,10 +29,11 @@ from .const import (
CONF_ON_OFFLINE,
CONF_ON_ONLINE,
CONF_REGISTER_COUNT,
CONF_REGISTER_LAST_ADDRESS,
CONF_REGISTER_TYPE,
CONF_REGISTER_VALUE,
CONF_RESPONSE_SIZE,
CONF_SERVER_COURTESY_RESPONSE,
CONF_SERVER_REGISTERS,
CONF_SKIP_UPDATES,
CONF_VALUE_TYPE,
)
@@ -38,6 +42,9 @@ CODEOWNERS = ["@martgras"]
AUTO_LOAD = ["modbus"]
CONF_READ_LAMBDA = "read_lambda"
CONF_WRITE_LAMBDA = "write_lambda"
CONF_SERVER_REGISTERS = "server_registers"
MULTI_CONF = True
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
@@ -46,9 +53,30 @@ ModbusController = modbus_controller_ns.class_(
)
SensorItem = modbus_controller_ns.struct("SensorItem")
ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse")
ServerRegister = modbus_controller_ns.struct("ServerRegister")
_LOGGER = logging.getLogger(__name__)
SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=False): cv.boolean,
cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t,
cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t,
}
)
ModbusServerRegisterSchema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ServerRegister),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
cv.Required(CONF_READ_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -57,16 +85,12 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_COMMAND_THROTTLE, default="0ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_SERVER_COURTESY_RESPONSE): cv.invalid(
"This option has been removed. Use modbus_server component instead: https://esphome.io/components/modbus_server/"
),
cv.Optional(CONF_SERVER_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA,
cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int,
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
cv.Optional(
CONF_SERVER_REGISTERS,
): cv.invalid(
"This option has been removed. Use modbus_server component instead: https://esphome.io/components/modbus_server/"
),
): cv.ensure_list(ModbusServerRegisterSchema),
cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation({}),
cv.Optional(CONF_ON_ONLINE): automation.validate_automation({}),
cv.Optional(CONF_ON_OFFLINE): automation.validate_automation({}),
@@ -118,9 +142,11 @@ def validate_modbus_register(config):
def _final_validate(config):
return modbus.final_validate_modbus_device("modbus_controller", role="client")(
config
)
if CONF_SERVER_COURTESY_RESPONSE in config or CONF_SERVER_REGISTERS in config:
return modbus.final_validate_modbus_device("modbus_controller", role="server")(
config
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -202,8 +228,53 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
if server_courtesy_response := config.get(CONF_SERVER_COURTESY_RESPONSE):
cg.add(
var.set_server_courtesy_response(
cg.StructInitializer(
ServerCourtesyResponse,
("enabled", server_courtesy_response[CONF_ENABLED]),
(
"register_last_address",
server_courtesy_response[CONF_REGISTER_LAST_ADDRESS],
),
("register_value", server_courtesy_response[CONF_REGISTER_VALUE]),
)
)
)
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
if CONF_SERVER_REGISTERS in config:
for server_register in config[CONF_SERVER_REGISTERS]:
server_register_var = cg.new_Pvariable(
server_register[CONF_ID],
server_register[CONF_ADDRESS],
server_register[CONF_VALUE_TYPE],
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
)
cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]]
cg.add(
server_register_var.set_read_lambda(
cg.TemplateArguments(cpp_type),
await cg.process_lambda(
server_register[CONF_READ_LAMBDA],
[(cg.uint16, "address")],
return_type=cpp_type,
),
)
)
if CONF_WRITE_LAMBDA in server_register:
cg.add(
server_register_var.set_write_lambda(
cg.TemplateArguments(cpp_type),
await cg.process_lambda(
server_register[CONF_WRITE_LAMBDA],
parameters=[(cg.uint16, "address"), (cpp_type, "x")],
return_type=cg.bool_,
),
)
)
cg.add(var.add_server_register(server_register_var))
await register_modbus_device(var, config)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@@ -18,7 +18,6 @@ CONF_REGISTER_TYPE = "register_type"
CONF_REGISTER_VALUE = "register_value"
CONF_RESPONSE_SIZE = "response_size"
CONF_SERVER_COURTESY_RESPONSE = "server_courtesy_response"
CONF_SERVER_REGISTERS = "server_registers"
CONF_SKIP_UPDATES = "skip_updates"
CONF_USE_WRITE_MULTIPLE = "use_write_multiple"
CONF_VALUE_TYPE = "value_type"
@@ -112,6 +112,167 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_
}
}
void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
uint16_t number_of_registers) {
ESP_LOGD(TAG,
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
std::vector<uint16_t> sixteen_bit_response;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool found = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
if (!server_register->read_lambda) {
break;
}
int64_t value = server_register->read_lambda();
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
server_register->address, static_cast<size_t>(server_register->value_type),
server_register->register_count, server_register->format_value(value).c_str());
std::vector<uint16_t> payload;
payload.reserve(server_register->register_count * 2);
modbus::helpers::number_to_payload(payload, value, server_register->value_type);
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
current_address += server_register->register_count;
found = true;
break;
}
}
if (!found) {
if (this->server_courtesy_response_.enabled &&
(current_address <= this->server_courtesy_response_.register_last_address)) {
ESP_LOGD(TAG,
"Could not match any register to address 0x%02X, but default allowed. "
"Returning default value: %d.",
current_address, this->server_courtesy_response_.register_value);
sixteen_bit_response.push_back(this->server_courtesy_response_.register_value);
current_address += 1; // Just increment by 1, as the default response is a single register
} else {
ESP_LOGW(TAG,
"Could not match any register to address 0x%02X and default not allowed. Sending exception response.",
current_address);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
}
}
std::vector<uint8_t> response;
for (auto v : sixteen_bit_response) {
auto decoded_value = decode_value(v);
response.push_back(decoded_value[0]);
response.push_back(decoded_value[1]);
}
this->send(function_code, start_address, number_of_registers, response.size(), response.data());
}
void ModbusController::on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) {
uint16_t number_of_registers;
uint16_t payload_offset;
if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
if (data.size() < 5) {
ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size());
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8);
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) {
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
uint16_t payload_size = data[4];
if (payload_size != number_of_registers * 2) {
ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.",
payload_size, number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
if (data.size() < 5 + payload_size) {
ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(),
5 + payload_size);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
payload_offset = 5;
} else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
if (data.size() < 4) {
ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size());
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
number_of_registers = 1;
payload_offset = 2;
} else {
ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return;
}
uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8);
ESP_LOGD(TAG,
"Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
auto for_each_register = [this, start_address, number_of_registers, payload_offset](
const std::function<bool(ServerRegister *, uint16_t offset)> &callback) -> bool {
uint16_t offset = payload_offset;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool ok = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
ok = callback(server_register, offset);
current_address += server_register->register_count;
offset += server_register->register_count * sizeof(uint16_t);
break;
}
}
if (!ok) {
return false;
}
}
return true;
};
// check all registers are writable before writing to any of them:
if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
return server_register->write_lambda != nullptr;
})) {
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return;
}
// Actually write to the registers:
if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) {
int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
return server_register->write_lambda(number);
})) {
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
return;
}
std::vector<uint8_t> response;
response.reserve(6);
response.push_back(this->address_);
response.push_back(function_code);
response.insert(response.end(), data.begin(), data.begin() + 4);
this->send_raw(response);
}
SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
auto reg_it = std::find_if(
std::begin(this->register_ranges_), std::end(this->register_ranges_),
@@ -311,8 +472,14 @@ void ModbusController::dump_config() {
"ModbusController:\n"
" Address: 0x%02X\n"
" Max Command Retries: %d\n"
" Offline Skip Updates: %d\n",
this->address_, this->max_cmd_retries_, this->offline_skip_updates_);
" Offline Skip Updates: %d\n"
" Server Courtesy Response:\n"
" Enabled: %s\n"
" Register Last Address: 0x%02X\n"
" Register Value: %d",
this->address_, this->max_cmd_retries_, this->offline_skip_updates_,
this->server_courtesy_response_.enabled ? "true" : "false",
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGCONFIG(TAG, "sensormap");
@@ -326,6 +493,11 @@ void ModbusController::dump_config() {
ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
it.start_address, it.register_count, it.skip_updates);
}
ESP_LOGCONFIG(TAG, "server registers");
for (auto &r : this->server_registers_) {
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
static_cast<uint8_t>(r->value_type), r->register_count);
}
#endif
}
@@ -120,6 +120,82 @@ class SensorItem {
bool force_new_range{false};
};
struct ServerCourtesyResponse {
bool enabled{false};
uint16_t register_last_address{0xFFFF};
uint16_t register_value{0};
};
class ServerRegister {
using ReadLambda = std::function<int64_t()>;
using WriteLambda = std::function<bool(int64_t value)>;
public:
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) {
this->address = address;
this->value_type = value_type;
this->register_count = register_count;
}
template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
this->read_lambda = [this, user_read_lambda]() -> int64_t {
T user_value = user_read_lambda(this->address);
if constexpr (std::is_same_v<T, float>) {
return bit_cast<uint32_t>(user_value);
} else {
return static_cast<int64_t>(user_value);
}
};
}
template<typename T>
void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
this->write_lambda = [this, user_write_lambda](int64_t number) {
if constexpr (std::is_same_v<T, float>) {
float float_value = bit_cast<float>(static_cast<uint32_t>(number));
return user_write_lambda(this->address, float_value);
}
return user_write_lambda(this->address, static_cast<T>(number));
};
}
// Formats a raw value into a string representation based on the value type for debugging
std::string format_value(int64_t value) const {
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
// plus null terminator = 43, rounded to 44 for 4-byte alignment
char buf[44];
switch (this->value_type) {
case SensorValueType::U_WORD:
case SensorValueType::U_DWORD:
case SensorValueType::U_DWORD_R:
case SensorValueType::U_QWORD:
case SensorValueType::U_QWORD_R:
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
return buf;
case SensorValueType::S_WORD:
case SensorValueType::S_DWORD:
case SensorValueType::S_DWORD_R:
case SensorValueType::S_QWORD:
case SensorValueType::S_QWORD_R:
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
return buf;
case SensorValueType::FP32_R:
case SensorValueType::FP32:
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
return buf;
default:
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
return buf;
}
}
uint16_t address{0};
SensorValueType value_type{SensorValueType::RAW};
uint8_t register_count{0};
ReadLambda read_lambda;
WriteLambda write_lambda;
};
// ModbusController::create_register_ranges_ tries to optimize register range
// for this the sensors must be ordered by register_type, start_address and bitmask
class SensorItemsComparator {
@@ -291,10 +367,16 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
void queue_command(const ModbusCommandItem &command);
/// Registers a sensor with the controller. Called by esphomes code generator
void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
/// Registers a server register with the controller. Called by esphomes code generator
void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
/// called when a modbus response was parsed without errors
void on_modbus_data(const std::vector<uint8_t> &data) override;
/// called when a modbus error response was received
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
/// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
/// called when a modbus request (function code 0x06 or 0x10) was parsed without errors
void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
/// default delegate called by process_modbus_data when a response has retrieved from the incoming queue
void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
/// default delegate called by process_modbus_data when a response for a write response has retrieved from the
@@ -331,6 +413,12 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
/// get how many times a command will be (re)sent if no response is received
uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
/// Called by esphome generated code to set the server courtesy response object
void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
this->server_courtesy_response_ = server_courtesy_response;
}
/// Get the server courtesy response object
ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }
protected:
/// parse sensormap_ and create range of sequential addresses
@@ -347,6 +435,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
void dump_sensors_();
/// Collection of all sensors for this component
SensorSet sensorset_;
/// Collection of all server registers for this component
std::vector<ServerRegister *> server_registers_{};
/// Continuous range of modbus registers
std::vector<RegisterRange> register_ranges_{};
/// Hold the pending requests to be sent
@@ -371,6 +461,9 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
CallbackManager<void(int, int)> online_callback_{};
/// Server offline callback
CallbackManager<void(int, int)> offline_callback_{};
/// Server courtesy response
ServerCourtesyResponse server_courtesy_response_{
.enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
};
/** Convert vector<uint8_t> response payload to float.
@@ -1,124 +0,0 @@
import esphome.codegen as cg
from esphome.components import modbus
from esphome.components.const import CONF_ENABLED
from esphome.components.modbus.helpers import (
CPP_TYPE_REGISTER_MAP,
SENSOR_VALUE_TYPE,
TYPE_REGISTER_MAP,
)
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID
from .const import (
CONF_COURTESY_RESPONSE,
CONF_READ_LAMBDA,
CONF_REGISTER_LAST_ADDRESS,
CONF_REGISTER_VALUE,
CONF_REGISTERS,
CONF_VALUE_TYPE,
CONF_WRITE_LAMBDA,
)
CODEOWNERS = ["@exciton"]
AUTO_LOAD = ["modbus"]
MULTI_CONF = True
modbus_server_ns = cg.esphome_ns.namespace("modbus_server")
ModbusServer = modbus_server_ns.class_(
"ModbusServer", cg.Component, modbus.ModbusDevice
)
ServerCourtesyResponse = modbus_server_ns.struct("ServerCourtesyResponse")
ServerRegister = modbus_server_ns.struct("ServerRegister")
SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=False): cv.boolean,
cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t,
cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t,
}
)
ModbusServerRegisterSchema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ServerRegister),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
cv.Required(CONF_READ_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ModbusServer),
cv.Optional(CONF_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA,
cv.Optional(
CONF_REGISTERS,
): cv.ensure_list(ModbusServerRegisterSchema),
}
).extend(modbus.modbus_device_schema(0x01)),
)
def _final_validate(config):
return modbus.final_validate_modbus_device("modbus_server", role="server")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
if server_courtesy_response := config.get(CONF_COURTESY_RESPONSE):
cg.add(
var.set_server_courtesy_response(
cg.StructInitializer(
ServerCourtesyResponse,
("enabled", server_courtesy_response[CONF_ENABLED]),
(
"register_last_address",
server_courtesy_response[CONF_REGISTER_LAST_ADDRESS],
),
("register_value", server_courtesy_response[CONF_REGISTER_VALUE]),
)
)
)
if CONF_REGISTERS in config:
for server_register in config[CONF_REGISTERS]:
server_register_var = cg.new_Pvariable(
server_register[CONF_ID],
server_register[CONF_ADDRESS],
server_register[CONF_VALUE_TYPE],
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
)
cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]]
cg.add(
server_register_var.set_read_lambda(
cg.TemplateArguments(cpp_type),
await cg.process_lambda(
server_register[CONF_READ_LAMBDA],
[(cg.uint16, "address")],
return_type=cpp_type,
),
)
)
if CONF_WRITE_LAMBDA in server_register:
cg.add(
server_register_var.set_write_lambda(
cg.TemplateArguments(cpp_type),
await cg.process_lambda(
server_register[CONF_WRITE_LAMBDA],
parameters=[(cg.uint16, "address"), (cpp_type, "x")],
return_type=cg.bool_,
),
)
)
cg.add(var.add_server_register(server_register_var))
cg.add(var.set_address(config[CONF_ADDRESS]))
await cg.register_component(var, config)
return await modbus.register_modbus_device(var, config)
@@ -1,7 +0,0 @@
CONF_REGISTER_LAST_ADDRESS = "register_last_address"
CONF_REGISTER_VALUE = "register_value"
CONF_VALUE_TYPE = "value_type"
CONF_COURTESY_RESPONSE = "courtesy_response"
CONF_READ_LAMBDA = "read_lambda"
CONF_WRITE_LAMBDA = "write_lambda"
CONF_REGISTERS = "registers"
@@ -1,192 +0,0 @@
#include "modbus_server.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome::modbus_server {
using modbus::ModbusFunctionCode;
using modbus::ModbusExceptionCode;
static const char *const TAG = "modbus_server";
void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
uint16_t number_of_registers) {
ESP_LOGD(TAG,
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
std::vector<uint16_t> sixteen_bit_response;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool found = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
if (!server_register->read_lambda) {
break;
}
int64_t value = server_register->read_lambda();
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
server_register->address, static_cast<size_t>(server_register->value_type),
server_register->register_count, server_register->format_value(value).c_str());
std::vector<uint16_t> payload;
payload.reserve(server_register->register_count * 2);
modbus::helpers::number_to_payload(payload, value, server_register->value_type);
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
current_address += server_register->register_count;
found = true;
break;
}
}
if (!found) {
if (this->server_courtesy_response_.enabled &&
(current_address <= this->server_courtesy_response_.register_last_address)) {
ESP_LOGD(TAG,
"Could not match any register to address 0x%02X, but default allowed. "
"Returning default value: %d.",
current_address, this->server_courtesy_response_.register_value);
sixteen_bit_response.push_back(this->server_courtesy_response_.register_value);
current_address += 1; // Just increment by 1, as the default response is a single register
} else {
ESP_LOGW(TAG,
"Could not match any register to address 0x%02X and default not allowed. Sending exception response.",
current_address);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
}
}
std::vector<uint8_t> response;
for (auto v : sixteen_bit_response) {
auto decoded_value = decode_value(v);
response.push_back(decoded_value[0]);
response.push_back(decoded_value[1]);
}
this->send(function_code, start_address, number_of_registers, response.size(), response.data());
}
void ModbusServer::on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) {
uint16_t number_of_registers;
uint16_t payload_offset;
if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
if (data.size() < 5) {
ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size());
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8);
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) {
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
uint16_t payload_size = data[4];
if (payload_size != number_of_registers * 2) {
ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.",
payload_size, number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
if (data.size() < 5 + payload_size) {
ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(),
5 + payload_size);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
payload_offset = 5;
} else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
if (data.size() < 4) {
ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size());
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
number_of_registers = 1;
payload_offset = 2;
} else {
ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return;
}
uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8);
ESP_LOGD(TAG,
"Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
auto for_each_register = [this, start_address, number_of_registers, payload_offset](
const std::function<bool(ServerRegister *, uint16_t offset)> &callback) -> bool {
uint16_t offset = payload_offset;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool ok = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
ok = callback(server_register, offset);
current_address += server_register->register_count;
offset += server_register->register_count * sizeof(uint16_t);
break;
}
}
if (!ok) {
return false;
}
}
return true;
};
// check all registers are writable before writing to any of them:
if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
return server_register->write_lambda != nullptr;
})) {
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return;
}
// Actually write to the registers:
if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) {
int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
return server_register->write_lambda(number);
})) {
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
return;
}
std::vector<uint8_t> response;
response.reserve(6);
response.push_back(this->address_);
response.push_back(function_code);
response.insert(response.end(), data.begin(), data.begin() + 4);
this->send_raw(response);
}
void ModbusServer::dump_config() {
ESP_LOGCONFIG(TAG,
"ModbusServer:\n"
" Address: 0x%02X\n"
" Server Courtesy Response:\n"
" Enabled: %s\n"
" Register Last Address: 0x%02X\n"
" Register Value: %" PRIu16,
this->address_, this->server_courtesy_response_.enabled ? "true" : "false",
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGCONFIG(TAG, "server registers");
for (auto &r : this->server_registers_) {
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
static_cast<uint8_t>(r->value_type), r->register_count);
}
#endif
}
} // namespace esphome::modbus_server
@@ -1,119 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/modbus/modbus.h"
#include "esphome/components/modbus/modbus_helpers.h"
#include "esphome/core/automation.h"
#include <utility>
#include <vector>
namespace esphome::modbus_server {
using modbus::helpers::SensorValueType;
struct ServerCourtesyResponse {
bool enabled{false};
uint16_t register_last_address{0xFFFF};
uint16_t register_value{0};
};
class ServerRegister {
using ReadLambda = std::function<int64_t()>;
using WriteLambda = std::function<bool(int64_t value)>;
public:
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) {
this->address = address;
this->value_type = value_type;
this->register_count = register_count;
}
template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
this->read_lambda = [this, user_read_lambda]() -> int64_t {
T user_value = user_read_lambda(this->address);
if constexpr (std::is_same_v<T, float>) {
return bit_cast<uint32_t>(user_value);
} else {
return static_cast<int64_t>(user_value);
}
};
}
template<typename T>
void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
this->write_lambda = [this, user_write_lambda](int64_t number) {
if constexpr (std::is_same_v<T, float>) {
float float_value = bit_cast<float>(static_cast<uint32_t>(number));
return user_write_lambda(this->address, float_value);
}
return user_write_lambda(this->address, static_cast<T>(number));
};
}
// Formats a raw value into a string representation based on the value type for debugging
std::string format_value(int64_t value) const {
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
// plus null terminator = 43, rounded to 44 for 4-byte alignment
char buf[44];
switch (this->value_type) {
case SensorValueType::U_WORD:
case SensorValueType::U_DWORD:
case SensorValueType::U_DWORD_R:
case SensorValueType::U_QWORD:
case SensorValueType::U_QWORD_R:
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
return buf;
case SensorValueType::S_WORD:
case SensorValueType::S_DWORD:
case SensorValueType::S_DWORD_R:
case SensorValueType::S_QWORD:
case SensorValueType::S_QWORD_R:
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
return buf;
case SensorValueType::FP32_R:
case SensorValueType::FP32:
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
return buf;
default:
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
return buf;
}
}
uint16_t address{0};
SensorValueType value_type{SensorValueType::RAW};
uint8_t register_count{0};
ReadLambda read_lambda;
WriteLambda write_lambda;
};
class ModbusServer : public Component, public modbus::ModbusDevice {
public:
void dump_config() override;
/// Not used for ModbusServer.
void on_modbus_data(const std::vector<uint8_t> &data) override{};
/// Registers a server register with the controller. Called by esphomes code generator
void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
/// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
/// called when a modbus request (function code 0x06 or 0x10) was parsed without errors
void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
/// Called by esphome generated code to set the server courtesy response object
void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
this->server_courtesy_response_ = server_courtesy_response;
}
/// Get the server courtesy response object
ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }
protected:
/// Collection of all server registers for this component
std::vector<ServerRegister *> server_registers_{};
/// Server courtesy response
ServerCourtesyResponse server_courtesy_response_{
.enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
};
} // namespace esphome::modbus_server
@@ -16,13 +16,6 @@ namespace esphome::nextion {
static const char *const TAG = "nextion.upload.arduino";
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
// Timeout for display acknowledgment during TFT upload (ms).
// A single value is used for all chunks; the happy path returns as soon as
// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field
// reports showed the previous 500ms steady-state value was too tight for
// some firmware variants.
static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000;
// Followed guide
// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
@@ -87,14 +80,14 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) {
recv_string.clear();
this->write_array(buffer, buffer_size);
App.feed_wdt();
this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true);
this->recv_ret_string_(recv_string, this->upload_first_chunk_sent_ ? 500 : 5000, true);
this->content_length_ -= read_len;
const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_,
EspClass::getFreeHeap());
this->upload_first_chunk_sent_ = true;
if (recv_string.empty()) {
ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS);
ESP_LOGW(TAG, "No response from display during upload");
allocator.deallocate(buffer, 4096);
buffer = nullptr;
return -1;
@@ -331,7 +324,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
#ifdef USE_ESP8266
WiFiClient *Nextion::get_wifi_client_() {
if (this->tft_url_.starts_with("https:")) {
if (this->tft_url_.compare(0, 6, "https:") == 0) {
if (this->wifi_client_secure_ == nullptr) {
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
this->wifi_client_secure_ = new BearSSL::WiFiClientSecure();
@@ -19,13 +19,6 @@ namespace esphome::nextion {
static const char *const TAG = "nextion.upload.esp32";
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
// Timeout for display acknowledgment during TFT upload (ms).
// A single value is used for all chunks; the happy path returns as soon as
// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field
// reports showed the previous 500ms steady-state value was too tight for
// some firmware variants.
static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000;
// Followed guide
// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
@@ -103,7 +96,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
recv_string.clear();
this->write_array(buffer, buffer_size);
App.feed_wdt();
this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true);
this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true);
this->content_length_ -= read_len;
const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
#ifdef USE_PSRAM
@@ -116,7 +109,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
#endif
upload_first_chunk_sent_ = true;
if (recv_string.empty()) {
ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS);
ESP_LOGW(TAG, "No response from display during upload");
allocator.deallocate(buffer, 4096);
buffer = nullptr;
return -1;
+8 -20
View File
@@ -141,22 +141,6 @@ CONF_UICR_ERASE = "uicr_erase"
VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3]
_DFU_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(DeviceFirmwareUpdate),
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
)
def _dfu_schema(value: bool | ConfigType) -> ConfigType:
if isinstance(value, bool):
if not value:
raise cv.Invalid("Use 'dfu: true' or specify a configuration dict")
return _DFU_SCHEMA({})
return _DFU_SCHEMA(value)
CONFIG_SCHEMA = cv.All(
_detect_bootloader,
set_core_data,
@@ -166,7 +150,12 @@ CONFIG_SCHEMA = cv.All(
cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH)
),
cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True),
cv.Optional(CONF_DFU): _dfu_schema,
cv.Optional(CONF_DFU): cv.Schema(
{
cv.GenerateID(): cv.declare_id(DeviceFirmwareUpdate),
cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
),
cv.Optional(CONF_DCDC, default=True): cv.boolean,
cv.Optional(CONF_REG0): cv.Schema(
{
@@ -332,9 +321,8 @@ async def to_code(config: ConfigType) -> None:
async def _dfu_to_code(dfu_config):
cg.add_define("USE_NRF52_DFU")
var = cg.new_Pvariable(dfu_config[CONF_ID])
if CONF_RESET_PIN in dfu_config:
pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN])
cg.add(var.set_reset_pin(pin))
pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN])
cg.add(var.set_reset_pin(pin))
zephyr_add_prj_conf("CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", True)
await cg.register_component(var, dfu_config)
+7 -20
View File
@@ -2,34 +2,24 @@
#ifdef USE_NRF52_DFU
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/components/zephyr/cdc_acm.h"
#include <hal/nrf_power.h>
namespace esphome::nrf52 {
namespace esphome {
namespace nrf52 {
static const char *const TAG = "dfu";
static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS
static const uint8_t DFU_MAGIC_UF2_RESET = 0x57; // Adafruit nRF52 bootloader UF2 magic
void DeviceFirmwareUpdate::setup() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
}
this->reset_pin_->setup();
#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT)
zephyr::global_cdc_acm->add_on_rate_callback([this](const device *, uint32_t rate) {
if (rate == 1200) {
volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C;
(*dbl_reset_mem) = DFU_DBL_RESET_MAGIC;
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(true);
} else {
NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET;
App.reboot();
}
this->reset_pin_->digital_write(true);
}
});
#endif
@@ -37,13 +27,10 @@ void DeviceFirmwareUpdate::setup() {
void DeviceFirmwareUpdate::dump_config() {
ESP_LOGCONFIG(TAG, "DFU:");
if (this->reset_pin_ != nullptr) {
LOG_PIN(" RESET Pin: ", this->reset_pin_);
} else {
ESP_LOGCONFIG(TAG, " Method: GPREGRET");
}
LOG_PIN(" RESET Pin: ", this->reset_pin_);
}
} // namespace esphome::nrf52
} // namespace nrf52
} // namespace esphome
#endif
+5 -3
View File
@@ -5,7 +5,8 @@
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
namespace esphome::nrf52 {
namespace esphome {
namespace nrf52 {
class DeviceFirmwareUpdate : public Component {
public:
void setup() override;
@@ -13,9 +14,10 @@ class DeviceFirmwareUpdate : public Component {
void dump_config() override;
protected:
GPIOPin *reset_pin_{nullptr};
GPIOPin *reset_pin_;
};
} // namespace esphome::nrf52
} // namespace nrf52
} // namespace esphome
#endif
+1 -2
View File
@@ -82,7 +82,6 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
setup_unit_of_measurement,
@@ -302,7 +301,7 @@ async def register_number(
):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
queue_entity_register("number", config)
cg.add(cg.App.register_number(var))
CORE.register_platform_component("number", var)
await setup_number_core_(
var, config, min_value=min_value, max_value=max_value, step=step
@@ -28,7 +28,7 @@ bool OnlineImage::validate_url_(const std::string &url) {
ESP_LOGE(TAG, "URL is too long");
return false;
}
if (!url.starts_with("http://") && !url.starts_with("https://")) {
if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) {
ESP_LOGE(TAG, "URL must start with http:// or https://");
return false;
}
+1 -9
View File
@@ -54,16 +54,10 @@ async def setup_output_platform_(obj, config):
power_supply_ = await cg.get_variable(config[CONF_POWER_SUPPLY])
cg.add(obj.set_power_supply(power_supply_))
if CONF_MAX_POWER in config:
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
cg.add(obj.set_max_power(config[CONF_MAX_POWER]))
if CONF_MIN_POWER in config:
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
cg.add(obj.set_min_power(config[CONF_MIN_POWER]))
# Only emit when zero_means_zero is actually enabled. The schema defaults to False
# so this key is always present; emitting unconditionally would force
# USE_OUTPUT_FLOAT_POWER_SCALING on for every output, defeating the gate.
if config.get(CONF_ZERO_MEANS_ZERO):
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
if CONF_ZERO_MEANS_ZERO in config:
cg.add(obj.set_zero_means_zero(config[CONF_ZERO_MEANS_ZERO]))
@@ -127,7 +121,6 @@ async def output_set_level_to_code(config, action_id, template_arg, args):
synchronous=True,
)
async def output_set_min_power_to_code(config, action_id, template_arg, args):
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_MIN_POWER], args, cg.float_)
@@ -147,7 +140,6 @@ async def output_set_min_power_to_code(config, action_id, template_arg, args):
synchronous=True,
)
async def output_set_max_power_to_code(config, action_id, template_arg, args):
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_MAX_POWER], args, cg.float_)
-3
View File
@@ -2,7 +2,6 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/defines.h"
#include "esphome/components/output/binary_output.h"
#include "esphome/components/output/float_output.h"
@@ -41,7 +40,6 @@ template<typename... Ts> class SetLevelAction : public Action<Ts...> {
FloatOutput *output_;
};
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
template<typename... Ts> class SetMinPowerAction : public Action<Ts...> {
public:
SetMinPowerAction(FloatOutput *output) : output_(output) {}
@@ -65,7 +63,6 @@ template<typename... Ts> class SetMaxPowerAction : public Action<Ts...> {
protected:
FloatOutput *output_;
};
#endif // USE_OUTPUT_FLOAT_POWER_SCALING
} // namespace output
} // namespace esphome
+2 -6
View File
@@ -7,15 +7,13 @@ namespace output {
static const char *const TAG = "output.float";
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
void FloatOutput::set_max_power(float max_power) {
this->max_power_ = clamp(max_power, this->min_power_, 1.0f); // Clamp to min_power <= max <= 1.0
this->max_power_ = clamp(max_power, this->min_power_, 1.0f); // Clamp to MIN>=MAX>=1.0
}
void FloatOutput::set_min_power(float min_power) {
this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0 <= min <= max_power
this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0>=MIN>=MAX
}
#endif
void FloatOutput::set_level(float state) {
state = clamp(state, 0.0f, 1.0f);
@@ -28,10 +26,8 @@ void FloatOutput::set_level(float state) {
}
#endif
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
if (state != 0.0f || !this->zero_means_zero_) // regardless of min_power_, 0.0 means off
state = (state * (this->max_power_ - this->min_power_)) + this->min_power_;
#endif
if (this->is_inverted())
state = 1.0f - state;
+3 -48
View File
@@ -1,13 +1,11 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "binary_output.h"
namespace esphome {
namespace output {
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
#define LOG_FLOAT_OUTPUT(this) \
LOG_BINARY_OUTPUT(this) \
if (this->max_power_ != 1.0f) { \
@@ -16,9 +14,6 @@ namespace output {
if (this->min_power_ != 0.0f) { \
ESP_LOGCONFIG(TAG, " Min Power: %.1f%%", this->min_power_ * 100.0f); \
}
#else
#define LOG_FLOAT_OUTPUT(this) LOG_BINARY_OUTPUT(this)
#endif
/** Base class for all output components that can output a variable level, like PWM.
*
@@ -27,18 +22,14 @@ namespace output {
* makes using maths much easier and (in theory) supports all possible bit depths.
*
* If you want to create a FloatOutput yourself, you essentially just have to override write_state(float).
* That method will be called for you with inversion already applied. When USE_OUTPUT_FLOAT_POWER_SCALING is
* enabled (set automatically by Python codegen if any output uses min_power/max_power/zero_means_zero or the
* matching runtime actions), the value will additionally have max-min power scaling and offset to min_power
* applied; otherwise only inversion is applied.
* That method will be called for you with inversion and max-min power and offset to min power already applied.
*
* This interface is compatible with BinaryOutput (and will automatically convert the binary states to floating
* point states for you). Additionally, this class provides a way for users to set a minimum and/or maximum power
* output (gated on USE_OUTPUT_FLOAT_POWER_SCALING).
* output
*/
class FloatOutput : public BinaryOutput {
public:
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
/** Set the maximum power output of this component.
*
* All values are multiplied by max_power - min_power and offset to min_power to get the adjusted value.
@@ -60,32 +51,6 @@ class FloatOutput : public BinaryOutput {
* @param zero_means_zero True if a 0 state should mean 0 and not min_power.
*/
void set_zero_means_zero(bool zero_means_zero) { this->zero_means_zero_ = zero_means_zero; }
#else
// Compile-time guards for users calling these methods from lambdas (documented usage at
// https://esphome.io/components/output/#output-set_min_power_action). When power scaling
// is compiled out, these template stubs fail to compile with an actionable error pointing
// at the user's lambda. Templating on a default-false bool means static_assert only fires
// on instantiation (i.e. when the user actually calls the method), not on every parse.
template<bool _use_output_float_power_scaling = false> void set_max_power(float max_power) {
static_assert(_use_output_float_power_scaling,
"set_max_power() requires USE_OUTPUT_FLOAT_POWER_SCALING. "
"To enable it, add 'max_power: 100%' (or any value) to one output entry in your YAML — "
"the codegen will then keep the scaling fields. "
"See https://esphome.io/components/output/ for details.");
}
template<bool _use_output_float_power_scaling = false> void set_min_power(float min_power) {
static_assert(_use_output_float_power_scaling,
"set_min_power() requires USE_OUTPUT_FLOAT_POWER_SCALING. "
"To enable it, add 'min_power: 0%' (or any value) to one output entry in your YAML — "
"the codegen will then keep the scaling fields. "
"See https://esphome.io/components/output/ for details.");
}
template<bool _use_output_float_power_scaling = false> void set_zero_means_zero(bool zero_means_zero) {
static_assert(_use_output_float_power_scaling,
"set_zero_means_zero() requires USE_OUTPUT_FLOAT_POWER_SCALING. "
"To enable it, add 'zero_means_zero: true' to one output entry in your YAML.");
}
#endif
/** Set the level of this float output, this is called from the front-end.
*
@@ -104,30 +69,20 @@ class FloatOutput : public BinaryOutput {
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
/// Get the maximum power output.
float get_max_power() const { return this->max_power_; }
/// Get the minimum power output.
float get_min_power() const { return this->min_power_; }
#else
/// Get the maximum power output.
float get_max_power() const { return 1.0f; }
/// Get the minimum power output.
float get_min_power() const { return 0.0f; }
#endif
protected:
/// Implement BinarySensor's write_enabled; this should never be called.
void write_state(bool state) override;
virtual void write_state(float state) = 0;
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
float max_power_{1.0f};
float min_power_{0.0f};
bool zero_means_zero_{false};
#endif
bool zero_means_zero_;
};
} // namespace output
+15 -24
View File
@@ -205,7 +205,7 @@ CONFIG_SCHEMA = cv.Any( # under `packages:` we can have either:
)
def _process_remote_package(config: dict[str, Any]) -> dict[str, Any]:
def _process_remote_package(config: dict, skip_update: bool = False) -> dict:
"""Clone/update a git repo and load the YAML files listed in the package definition.
Returns ``{"packages": {<filename>: <loaded_yaml>, ...}}`` so the caller
@@ -215,10 +215,11 @@ def _process_remote_package(config: dict[str, Any]) -> dict[str, Any]:
If loading fails after cloning, attempts a revert and retry in case
a prior cached checkout is stale.
"""
actual_refresh = git.NEVER_REFRESH if skip_update else config[CONF_REFRESH]
repo_dir, revert = git.clone_or_update(
url=config[CONF_URL],
ref=config.get(CONF_REF),
refresh=config[CONF_REFRESH],
refresh=actual_refresh,
domain=DOMAIN,
username=config.get(CONF_USERNAME),
password=config.get(CONF_PASSWORD),
@@ -377,8 +378,9 @@ def _substitute_package_definition(
Local package contents are left untouched they will be substituted
later during the main substitution pass.
"""
def do_substitute(package_config: dict | str) -> dict | str:
if isinstance(package_config, str) or (
isinstance(package_config, dict) and is_remote_package(package_config)
):
# Collect undefined-variable errors (rather than raising strict) so the
# path walked through a remote-package dict is preserved and the user
# sees which field (url / path / ref / ...) referenced the undefined
@@ -392,22 +394,6 @@ def _substitute_package_definition(
errors=errors,
)
raise_first_undefined(errors, "package definition")
return package_config
if isinstance(package_config, str):
return do_substitute(package_config)
if isinstance(package_config, dict) and is_remote_package(package_config):
# Mark vars as literal to avoid substituting variables in the vars block itself, since they are meant to be
# passed as-is to the package YAML and may contain their own substitution expressions that should not
# be prematurely evaluated here.
if CONF_FILES in package_config:
for file_def in package_config[CONF_FILES]:
if isinstance(file_def, dict) and CONF_VARS in file_def:
file_def[CONF_VARS] = yaml_util.make_literal(file_def[CONF_VARS])
package_config = do_substitute(package_config)
return package_config
@@ -455,9 +441,11 @@ class _PackageProcessor:
self,
substitutions: UserDict,
command_line_substitutions: dict[str, Any] | None,
skip_update: bool,
) -> None:
self.substitutions = substitutions
self.parent_context = UserDict(command_line_substitutions or {})
self.skip_update = skip_update
def resolve_package(
self,
@@ -505,7 +493,7 @@ class _PackageProcessor:
)
if is_remote_package(package_config):
package_config = _process_remote_package(package_config)
package_config = _process_remote_package(package_config, self.skip_update)
return package_config
def collect_substitutions(self, package_config: dict) -> None:
@@ -549,10 +537,11 @@ class _PackageProcessor:
def do_packages_pass(
config: dict[str, Any],
config: dict,
*,
command_line_substitutions: dict[str, Any] | None = None,
) -> dict[str, Any]:
skip_update: bool = False,
) -> dict:
"""Load, validate, and flatten all packages in the config.
Returns the config with all packages loaded in-place (but not yet merged)
@@ -567,7 +556,9 @@ def do_packages_pass(
config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions
)
)
processor = _PackageProcessor(substitutions, command_line_substitutions)
processor = _PackageProcessor(
substitutions, command_line_substitutions, skip_update
)
_update_substitutions_context(processor.parent_context, substitutions)
context_vars = push_context(
+3 -7
View File
@@ -433,17 +433,13 @@ void Pipsolar::handle_qpigs_(const char *message) {
}
void Pipsolar::handle_qmod_(const char *message) {
std::string mode;
char device_mode = char(message[1]);
if (this->last_qmod_) {
this->last_qmod_->publish_state(message);
}
// QMOD response is "(M" where M is the device-mode character. Bail out if the
// message is shorter than 2 chars (e.g. empty error response from
// handle_poll_error_) — reading message[1] would otherwise be out of bounds.
if (message[0] == '\0' || message[1] == '\0')
return;
if (this->device_mode_) {
std::string mode;
mode = char(message[1]);
mode = device_mode;
this->device_mode_->publish_state(mode);
}
}
+1 -2
View File
@@ -317,7 +317,6 @@ enum PN532ReadReady PN532::read_ready_(bool block) {
if (!this->rd_start_time_.has_value()) {
this->rd_start_time_ = millis();
}
const uint32_t rd_start_time = *this->rd_start_time_;
while (true) {
if (this->is_read_ready()) {
@@ -325,7 +324,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) {
break;
}
if (millis() - rd_start_time > 100) {
if (millis() - *this->rd_start_time_ > 100) {
ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!");
this->rd_ready_ = TIMEOUT;
break;

Some files were not shown because too many files have changed in this diff Show More