diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28174ebb6e..751241f563 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ permissions: env: DEFAULT_PYTHON: "3.12" - PYUPGRADE_TARGET: "--py311-plus" + PYUPGRADE_TARGET: "--py312-plus" concurrency: # yamllint disable-line rule:line-length diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba74aff07c..da424f516f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: rev: v3.21.2 hooks: - id: pyupgrade - args: [--py311-plus] + args: [--py312-plus] - repo: https://github.com/adrienverge/yamllint.git rev: v1.37.1 hooks: diff --git a/AGENTS.md b/AGENTS.md index 21905ea356..46caea3aec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ This document provides essential context for AI models interacting with this pro ## 2. Core Technologies & Stack -* **Languages:** Python (>=3.11), C++ (gnu++20) +* **Languages:** Python (>=3.12), C++ (gnu++20) * **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF. * **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative. * **Configuration:** YAML. diff --git a/docker/Dockerfile b/docker/Dockerfile index 5626d18fcc..1085076137 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.0.20 +RUN uv pip install --no-cache-dir esphome-device-builder==1.0.21 RUN \ platformio settings set enable_telemetry No \ diff --git a/esphome/async_thread.py b/esphome/async_thread.py index c5225a7a14..3972d735f5 100644 --- a/esphome/async_thread.py +++ b/esphome/async_thread.py @@ -12,12 +12,9 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable import threading -from typing import Generic, TypeVar - -_T = TypeVar("_T") -class AsyncThreadRunner(threading.Thread, Generic[_T]): +class AsyncThreadRunner[T](threading.Thread): """Run an async coroutine in a daemon thread and expose its result. The runner catches all exceptions from the coroutine and stores them in @@ -35,10 +32,10 @@ class AsyncThreadRunner(threading.Thread, Generic[_T]): result = runner.result """ - def __init__(self, coro_factory: Callable[[], Awaitable[_T]]) -> None: + def __init__(self, coro_factory: Callable[[], Awaitable[T]]) -> None: super().__init__(daemon=True) self._coro_factory = coro_factory - self.result: _T | None = None + self.result: T | None = None self.exception: BaseException | None = None self.event = threading.Event() diff --git a/esphome/components/a01nyub/a01nyub.cpp b/esphome/components/a01nyub/a01nyub.cpp index 344456854b..6111af2b7e 100644 --- a/esphome/components/a01nyub/a01nyub.cpp +++ b/esphome/components/a01nyub/a01nyub.cpp @@ -25,7 +25,7 @@ void A01nyubComponent::check_buffer_() { if (this->buffer_[3] == checksum) { float distance = (this->buffer_[1] << 8) + this->buffer_[2]; if (distance > 280) { - float meters = distance / 1000.0; + float meters = distance / 1000.0f; ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters); this->publish_state(meters); } else { diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index 3e21d6981d..477962a040 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -216,7 +216,7 @@ void AcDimmer::setup() { } void AcDimmer::write_state(float state) { - state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation + state = std::acos(1 - (2 * state)) / std::numbers::pi_v; // RMS power compensation auto new_value = static_cast(roundf(state * 65535)); if (new_value != 0 && this->store_.value == 0) this->store_.init_cycle = this->init_with_half_cycle_; diff --git a/esphome/components/am43/cover/am43_cover.cpp b/esphome/components/am43/cover/am43_cover.cpp index 35366dbaa6..4b096983a4 100644 --- a/esphome/components/am43/cover/am43_cover.cpp +++ b/esphome/components/am43/cover/am43_cover.cpp @@ -114,13 +114,13 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->decoder_->decode(param->notify.value, param->notify.value_len); if (this->decoder_->has_position()) { - this->position = ((float) this->decoder_->position_ / 100.0); + this->position = ((float) this->decoder_->position_ / 100.0f); if (!this->invert_position_) this->position = 1 - this->position; - if (this->position > 0.97) - this->position = 1.0; - if (this->position < 0.02) - this->position = 0.0; + if (this->position > 0.97f) + this->position = 1.0f; + if (this->position < 0.02f) + this->position = 0.0f; this->publish_state(); } diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp index 84dd4393eb..806a441dcd 100644 --- a/esphome/components/anova/anova_base.cpp +++ b/esphome/components/anova/anova_base.cpp @@ -6,9 +6,9 @@ namespace esphome::anova { -float ftoc(float f) { return (f - 32.0) * (5.0f / 9.0f); } +float ftoc(float f) { return (f - 32.0f) * (5.0f / 9.0f); } -float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0; } +float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0f; } AnovaPacket *AnovaCodec::clean_packet_() { this->packet_.length = strlen((char *) this->packet_.data); diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 091f496e33..d87f32fc36 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -395,7 +395,7 @@ async def to_code(config): ) if data.mp3_support: cg.add_define("USE_AUDIO_MP3_SUPPORT") - add_idf_component(name="esphome/micro-mp3", ref="0.3.0") + add_idf_component(name="esphome/micro-mp3", ref="0.4.0") _emit_memory_pair( data.mp3.buffer_memory, "CONFIG_MICRO_MP3_PREFER_PSRAM", diff --git a/esphome/components/binary_sensor_map/binary_sensor_map.cpp b/esphome/components/binary_sensor_map/binary_sensor_map.cpp index 316d44ba59..3185f15697 100644 --- a/esphome/components/binary_sensor_map/binary_sensor_map.cpp +++ b/esphome/components/binary_sensor_map/binary_sensor_map.cpp @@ -112,7 +112,7 @@ float BinarySensorMap::bayesian_predicate_(bool sensor_state, float prior, float prob_state_source_false = 1 - prob_given_false; } - return prob_state_source_true / (prior * prob_state_source_true + (1.0 - prior) * prob_state_source_false); + return prob_state_source_true / (prior * prob_state_source_true + (1.0f - prior) * prob_state_source_false); } void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float value) { diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index d387757051..9a27cffd04 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -205,7 +205,7 @@ void BL0906::read_data_(const uint8_t address, const float reference, sensor::Se // Chip temperature if (reference == BL0906_TREF) { value = (float) to_int32_t(data_s24); - value = (value - 64) * 12.5 / 59 - 40; + value = (value - 64) * 12.5f / 59 - 40; } sensor->publish_state(value); } diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp index b7df603f2f..642368d93f 100644 --- a/esphome/components/bl0940/bl0940.cpp +++ b/esphome/components/bl0940/bl0940.cpp @@ -120,7 +120,7 @@ float BL0940::calculate_power_reference_() { float BL0940::calculate_energy_reference_() { // formula: 3600000 * 4046 * RL * R1 * 1000 / (1638.4 * 256) / Vref² / (R1 + R2) // or: power_reference_ * 3600000 / (1638.4 * 256) - return this->power_reference_cal_ * 3600000 / (1638.4 * 256); + return this->power_reference_cal_ * 3600000 / (1638.4f * 256); } float BL0940::calculate_calibration_value_(float state) { return (100 + state) / 100; } diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 1c57616c82..e952df21be 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -124,14 +124,14 @@ void BL0942::setup() { // If either current or voltage references are set explicitly by the user, // calculate the power reference from it unless that is also explicitly set. if ((this->current_reference_set_ || this->voltage_reference_set_) && !this->power_reference_set_) { - this->power_reference_ = (this->voltage_reference_ * this->current_reference_ * 3537.0 / 305978.0) / 73989.0; + this->power_reference_ = (this->voltage_reference_ * this->current_reference_ * 3537.0f / 305978.0f) / 73989.0f; this->power_reference_set_ = true; } // Similarly for energy reference, if the power reference was set by the user // either implicitly or explicitly. if (this->power_reference_set_ && !this->energy_reference_set_) { - this->energy_reference_ = this->power_reference_ * 3600000 / 419430.4; + this->energy_reference_ = this->power_reference_ * 3600000 / 419430.4f; this->energy_reference_set_ = true; } diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index ddf1a105e0..8ef0976e3b 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -204,7 +204,7 @@ void MedianCombinationComponent::handle_new_value(float value) { median = sensor_states[sensor_states_size / 2]; } else { // Even number of measurements, use the average of the two middle measurements - median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0; + median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0f; } } diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 5a499d54a4..d15b310a37 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -39,7 +39,7 @@ void CurrentBasedCover::control(const CoverCall &call) { auto opt_pos = call.get_position(); if (opt_pos.has_value()) { auto pos = *opt_pos; - if (fabsf(this->position - pos) < 0.01) { + if (fabsf(this->position - pos) < 0.01f) { // already at target } else { auto op = pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING; diff --git a/esphome/components/daikin_brc/daikin_brc.cpp b/esphome/components/daikin_brc/daikin_brc.cpp index 5fe3d30a85..1b085013f1 100644 --- a/esphome/components/daikin_brc/daikin_brc.cpp +++ b/esphome/components/daikin_brc/daikin_brc.cpp @@ -151,7 +151,7 @@ uint8_t DaikinBrcClimate::temperature_() { // Temperature in remote is in F if (this->fahrenheit_) { temperature = (uint8_t) roundf( - clamp(((this->target_temperature * 1.8) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F)); + clamp(((this->target_temperature * 1.8f) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F)); } else { temperature = ((uint8_t) roundf(this->target_temperature) - 9) << 1; } diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index 35488eab03..ab4a8c458f 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -138,7 +138,7 @@ float DallasTemperatureSensor::get_temp_c_() { if (this->scratch_pad_[7] == 0) { return NAN; } - return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25; + return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25f; } switch (this->resolution_) { case 9: diff --git a/esphome/components/demo/demo_sensor.h b/esphome/components/demo/demo_sensor.h index 6153c810e1..ff2163776c 100644 --- a/esphome/components/demo/demo_sensor.h +++ b/esphome/components/demo/demo_sensor.h @@ -15,7 +15,7 @@ class DemoSensor final : public sensor::Sensor, public PollingComponent { float base = std::isnan(this->state) ? 0.0f : this->state; this->publish_state(base + val * 10); } else { - if (val < 0.1) { + if (val < 0.1f) { this->publish_state(NAN); } else { this->publish_state(val * 100); diff --git a/esphome/components/demo/demo_switch.h b/esphome/components/demo/demo_switch.h index 6846b8b663..dea975a770 100644 --- a/esphome/components/demo/demo_switch.h +++ b/esphome/components/demo/demo_switch.h @@ -9,7 +9,7 @@ namespace esphome::demo { class DemoSwitch final : public switch_::Switch, public Component { public: void setup() override { - bool initial = random_float() < 0.5; + bool initial = random_float() < 0.5f; this->publish_state(initial); } diff --git a/esphome/components/demo/demo_text_sensor.h b/esphome/components/demo/demo_text_sensor.h index fa728903d9..8eaa6c6b46 100644 --- a/esphome/components/demo/demo_text_sensor.h +++ b/esphome/components/demo/demo_text_sensor.h @@ -10,9 +10,9 @@ class DemoTextSensor final : public text_sensor::TextSensor, public PollingCompo public: void update() override { float val = random_float(); - if (val < 0.33) { + if (val < 0.33f) { this->publish_state("foo"); - } else if (val < 0.66) { + } else if (val < 0.66f) { this->publish_state("bar"); } else { this->publish_state("foobar"); diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp index 29ee166f51..570bfef943 100644 --- a/esphome/components/dfrobot_sen0395/commands.cpp +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -121,51 +121,51 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float this->cmd_ = "detRangeCfg -1 0 0"; } else if (min2 < 0 || max2 < 0) { - this->min1_ = min1 = round(min1 / 0.15) * 0.15; - this->max1_ = max1 = round(max1 / 0.15) * 0.15; + this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f; + this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f; this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1; char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null - snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15); + snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15f, max1 / 0.15f); this->cmd_ = buf; } else if (min3 < 0 || max3 < 0) { - this->min1_ = min1 = round(min1 / 0.15) * 0.15; - this->max1_ = max1 = round(max1 / 0.15) * 0.15; - this->min2_ = min2 = round(min2 / 0.15) * 0.15; - this->max2_ = max2 = round(max2 / 0.15) * 0.15; + this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f; + this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f; + this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f; + this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f; this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1; char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null - snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, - max2 / 0.15); + snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f, + max2 / 0.15f); this->cmd_ = buf; } else if (min4 < 0 || max4 < 0) { - this->min1_ = min1 = round(min1 / 0.15) * 0.15; - this->max1_ = max1 = round(max1 / 0.15) * 0.15; - this->min2_ = min2 = round(min2 / 0.15) * 0.15; - this->max2_ = max2 = round(max2 / 0.15) * 0.15; - this->min3_ = min3 = round(min3 / 0.15) * 0.15; - this->max3_ = max3 = round(max3 / 0.15) * 0.15; + this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f; + this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f; + this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f; + this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f; + this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f; + this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f; this->min4_ = min4 = this->max4_ = max4 = -1; char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null - snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, - max2 / 0.15, min3 / 0.15, max3 / 0.15); + snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, min2 / 0.15f, + max2 / 0.15f, min3 / 0.15f, max3 / 0.15f); this->cmd_ = buf; } else { - this->min1_ = min1 = round(min1 / 0.15) * 0.15; - this->max1_ = max1 = round(max1 / 0.15) * 0.15; - this->min2_ = min2 = round(min2 / 0.15) * 0.15; - this->max2_ = max2 = round(max2 / 0.15) * 0.15; - this->min3_ = min3 = round(min3 / 0.15) * 0.15; - this->max3_ = max3 = round(max3 / 0.15) * 0.15; - this->min4_ = min4 = round(min4 / 0.15) * 0.15; - this->max4_ = max4 = round(max4 / 0.15) * 0.15; + this->min1_ = min1 = roundf(min1 / 0.15f) * 0.15f; + this->max1_ = max1 = roundf(max1 / 0.15f) * 0.15f; + this->min2_ = min2 = roundf(min2 / 0.15f) * 0.15f; + this->max2_ = max2 = roundf(max2 / 0.15f) * 0.15f; + this->min3_ = min3 = roundf(min3 / 0.15f) * 0.15f; + this->max3_ = max3 = roundf(max3 / 0.15f) * 0.15f; + this->min4_ = min4 = roundf(min4 / 0.15f) * 0.15f; + this->max4_ = max4 = roundf(max4 / 0.15f) * 0.15f; char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null - snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, - min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, max4 / 0.15); + snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15f, max1 / 0.15f, + min2 / 0.15f, max2 / 0.15f, min3 / 0.15f, max3 / 0.15f, min4 / 0.15f, max4 / 0.15f); this->cmd_ = buf; } diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index b24c099bce..115adf503a 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -42,10 +42,10 @@ void Display::line_at_angle(int x, int y, int angle, int length, Color color) { void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_radius, Color color) { // Calculate start and end points - int x1 = (start_radius * cos(angle * M_PI / 180)) + x; - int y1 = (start_radius * sin(angle * M_PI / 180)) + y; - int x2 = (stop_radius * cos(angle * M_PI / 180)) + x; - int y2 = (stop_radius * sin(angle * M_PI / 180)) + y; + int x1 = (start_radius * std::cos(angle * std::numbers::pi_v / 180)) + x; + int y1 = (start_radius * std::sin(angle * std::numbers::pi_v / 180)) + y; + int x2 = (stop_radius * std::cos(angle * std::numbers::pi_v / 180)) + x; + int y2 = (stop_radius * std::sin(angle * std::numbers::pi_v / 180)) + y; // Draw line this->line(x1, y1, x2, y2, color); @@ -228,7 +228,7 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, int e2max, e2min; progress = std::max(0, std::min(progress, 100)); // 0..100 int draw_progress = progress > 50 ? (100 - progress) : progress; - float tan_a = (progress == 50) ? 65535 : tan(float(draw_progress) * M_PI / 100); // slope + float tan_a = (progress == 50) ? 65535 : tanf(float(draw_progress) * std::numbers::pi_v / 100); // slope do { // outer dots @@ -444,15 +444,15 @@ void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int * // hence we rotate the shape by 270° to orient the polygon up. rotation_degrees += ROTATION_270_DEGREES; // Convert the rotation to radians, easier to use in trigonometrical calculations - float rotation_radians = rotation_degrees * std::numbers::pi / 180; + float rotation_radians = rotation_degrees * std::numbers::pi_v / 180; // A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no // additional rotation of the shape. // A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal, // this requires to rotate the shape by π/edges radians counter-clockwise so that the first point is located on the // left side of the first horizontal edge. - rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi / edges : 0.0; + rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi_v / edges : 0.0f; - float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi + rotation_radians; + float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi_v + rotation_radians; *vertex_x = (int) std::round(std::cos(vertex_angle) * radius) + center_x; *vertex_y = (int) std::round(std::sin(vertex_angle) * radius) + center_y; } diff --git a/esphome/components/ds2484/ds2484.h b/esphome/components/ds2484/ds2484.h index b3337539ce..819b9456c1 100644 --- a/esphome/components/ds2484/ds2484.h +++ b/esphome/components/ds2484/ds2484.h @@ -12,7 +12,7 @@ class DS2484OneWireBus final : public one_wire::OneWireBus, public i2c::I2CDevic public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::BUS - 1.0; } + float get_setup_priority() const override { return setup_priority::BUS - 1.0f; } bool reset_device(); int reset_int() override; diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index bbd966fbe0..892b67b270 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -169,14 +169,14 @@ bool ES7210::configure_mic_gain_() { uint8_t ES7210::es7210_gain_reg_value_(float mic_gain) { // reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB - mic_gain += 0.5; - if (mic_gain <= 33.0) { + mic_gain += 0.5f; + if (mic_gain <= 33.0f) { return (uint8_t) (mic_gain / 3); } - if (mic_gain < 36.0) { + if (mic_gain < 36.0f) { return 12; } - if (mic_gain < 37.0) { + if (mic_gain < 37.0f) { return 13; } return 14; diff --git a/esphome/components/es7243e/es7243e.cpp b/esphome/components/es7243e/es7243e.cpp index b4d9fba4c5..fc3cba7ae4 100644 --- a/esphome/components/es7243e/es7243e.cpp +++ b/esphome/components/es7243e/es7243e.cpp @@ -105,14 +105,14 @@ bool ES7243E::configure_mic_gain_() { uint8_t ES7243E::es7243e_gain_reg_value_(float mic_gain) { // reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB - mic_gain += 0.5; - if (mic_gain <= 33.0) { + mic_gain += 0.5f; + if (mic_gain <= 33.0f) { return (uint8_t) mic_gain / 3; } - if (mic_gain < 36.0) { + if (mic_gain < 36.0f) { return 12; } - if (mic_gain < 37.0) { + if (mic_gain < 37.0f) { return 13; } return 14; diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index f89b4a2ff1..2756b615a1 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -303,7 +303,9 @@ void ESPNowComponent::loop() { ESP_LOGV(TAG, ">>> [%s] %s", addr_buf, LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status))); #endif if (this->current_send_packet_ != nullptr) { - this->current_send_packet_->callback_(packet->packet_.sent.status); + if (this->current_send_packet_->callback_ != nullptr) { + this->current_send_packet_->callback_(packet->packet_.sent.status); + } this->send_packet_pool_.release(this->current_send_packet_); this->current_send_packet_ = nullptr; // Reset current packet after sending } diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 5d59f60509..9ceb2f2ba0 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -139,7 +139,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo /// Draw grid if (!std::isnan(this->gridspacing_y_)) { for (int y = yn; y <= ym; y++) { - int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0 - (float) (y - yn) / (ym - yn))); + int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0f - (float) (y - yn) / (ym - yn))); for (uint32_t x = 0; x < this->width_; x += 2) { buff->draw_pixel_at(x_offset + x, y_offset + py, color); } @@ -177,7 +177,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick); bool b = (trace->get_line_type() & bit) == bit; if (b) { - int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset; + int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0f - v)) - thick / 2 + y_offset; auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) { if (y >= y_offset && static_cast(y) < y_offset + this->height_) buff->draw_pixel_at(x, y, c); diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp index eaa1440c4d..2c68eef623 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -122,7 +122,7 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, rpm = clamp(rpm, 1, 300); - ms_per_step = (uint16_t) (3000.0 / (float) rpm); + ms_per_step = (uint16_t) (3000.0f / (float) rpm); buffer_[0] = mode; buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw buffer_[2] = steps; @@ -153,7 +153,7 @@ void GroveMotorDriveTB6612FNG::stepper_keep_run(StepperModeTypeT mode, uint16_t uint16_t ms_per_step = 0; rpm = clamp(rpm, 1, 300); - ms_per_step = (uint16_t) (3000.0 / (float) rpm); + ms_per_step = (uint16_t) (3000.0f / (float) rpm); buffer_[0] = mode; buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 0ad9b00ce4..f68404afd9 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -607,7 +607,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { if (climate_control.target_temperature.has_value()) { float target_temp = climate_control.target_temperature.value(); out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16 - out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; + out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49f) ? 1 : 0; } if (out_data->ac_power == 0) { // If AC is off - no presets allowed diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index bd5678a425..a013371649 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -341,7 +341,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { if (climate_control.target_temperature.has_value()) { float target_temp = climate_control.target_temperature.value(); out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16 - out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; + out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49f) ? 1 : 0; } if (out_data->ac_power == 0) { // If AC is off - no presets allowed diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index 7930df7a38..c6b7da6610 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -2,6 +2,8 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" +#include + namespace esphome::hmc5883l { static const char *const TAG = "hmc5883l"; @@ -126,7 +128,7 @@ void HMC5883LComponent::update() { const float y = int16_t(raw_y) * mg_per_bit * 0.1f; const float z = int16_t(raw_z) * mg_per_bit * 0.1f; - float heading = atan2f(0.0f - x, y) * 180.0f / M_PI; + float heading = atan2f(0.0f - x, y) * 180.0f / std::numbers::pi_v; ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f°", x, y, z, heading); if (this->x_sensor_ != nullptr) diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp index 8bfc5e4f4f..dd86b95c78 100644 --- a/esphome/components/honeywellabp/honeywellabp.cpp +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -55,7 +55,9 @@ float HONEYWELLABPSensor::countstopressure_(const int counts, const float min_pr // Converts a digital temperature measurement in counts to temperature in C // This will be invalid if sensore daoes not have temperature measurement capability -float HONEYWELLABPSensor::countstotemperatures_(const int counts) { return (((float) counts / 2047.0) * 200.0) - 50.0; } +float HONEYWELLABPSensor::countstotemperatures_(const int counts) { + return (((float) counts / 2047.0f) * 200.0f) - 50.0f; +} // Pressure value from the most recent reading in units float HONEYWELLABPSensor::read_pressure_() { @@ -69,9 +71,9 @@ void HONEYWELLABPSensor::update() { ESP_LOGV(TAG, "Update Honeywell ABP Sensor"); if (readsensor_() == 0) { if (this->pressure_sensor_ != nullptr) - this->pressure_sensor_->publish_state(read_pressure_() * 1.0); + this->pressure_sensor_->publish_state(read_pressure_() * 1.0f); if (this->temperature_sensor_ != nullptr) - this->temperature_sensor_->publish_state(read_temperature_() * 1.0); + this->temperature_sensor_->publish_state(read_temperature_() * 1.0f); } } diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp index 0b3a746c34..270bb2709d 100644 --- a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp @@ -55,7 +55,7 @@ void HrxlMaxsonarWrComponent::check_buffer_() { millimeters = millimeters * 10; } - float meters = float(millimeters) / 1000.0; + float meters = float(millimeters) / 1000.0f; ESP_LOGV(TAG, "Distance from sensor: %d mm, %f m", millimeters, meters); this->publish_state(meters); } else { diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index c6ff42495f..5e271e671e 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -139,7 +139,7 @@ void I2SAudioSpeakerBase::set_volume(float volume) { this->volume_ = volume; #ifdef USE_AUDIO_DAC if (this->audio_dac_ != nullptr) { - if (volume > 0.0) { + if (volume > 0.0f) { this->audio_dac_->set_mute_off(); } this->audio_dac_->set_volume(volume); diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 85da196584..833d1989c8 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -119,7 +119,7 @@ void INA219Component::setup() { } this->calibration_lsb_ = lsb; - auto calibration = uint32_t(0.04096f / (0.000001 * lsb * this->shunt_resistance_ohm_)); + auto calibration = uint32_t(0.04096f / (0.000001f * lsb * this->shunt_resistance_ohm_)); ESP_LOGV(TAG, " Using LSB=%" PRIu32 " calibration=%" PRIu32, lsb, calibration); if (!this->write_byte_16(INA219_REGISTER_CALIBRATION, calibration)) { this->mark_failed(); diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp index 695de57c61..c22237d144 100644 --- a/esphome/components/ina226/ina226.cpp +++ b/esphome/components/ina226/ina226.cpp @@ -70,7 +70,7 @@ void INA226Component::setup() { this->calibration_lsb_ = lsb; - auto calibration = uint32_t(0.00512 / (lsb * this->shunt_resistance_ohm_ / 1000000.0f)); + auto calibration = uint32_t(0.00512f / (lsb * this->shunt_resistance_ohm_ / 1000000.0f)); ESP_LOGV(TAG, " Using LSB=%" PRIu32 " calibration=%" PRIu32, lsb, calibration); diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 5cafa9fe82..e431a06df6 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -315,14 +315,14 @@ class LightColorValues { if (this->color_temperature_ <= 0) { return this->color_temperature_; } - return 1000000.0 / this->color_temperature_; + return 1000000.0f / this->color_temperature_; } /// Set the color temperature property of these light color values in kelvin. void set_color_temperature_kelvin(float color_temperature) { if (color_temperature <= 0) { return; } - this->color_temperature_ = 1000000.0 / color_temperature; + this->color_temperature_ = 1000000.0f / color_temperature; } /// Get the cold white property of these light color values. In range 0.0 to 1.0. diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index 61fe098ad7..34e192a034 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -47,7 +47,7 @@ class LightTransitionTransformer : public LightTransformer { LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_; LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->end_values_; if (this->changing_color_mode_) - p = p < 0.5f ? p * 2 : (p - 0.5) * 2; + p = p < 0.5f ? p * 2 : (p - 0.5f) * 2; float v = LightTransformer::smoothed_progress(p); return LightColorValues::lerp(start, end, v); diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp index 62a0d2290a..dd78b20f2c 100644 --- a/esphome/components/ltr390/ltr390.cpp +++ b/esphome/components/ltr390/ltr390.cpp @@ -75,7 +75,7 @@ void LTR390Component::read_als_() { uint32_t als = *val; if (this->light_sensor_ != nullptr) { - float lux = ((0.6 * als) / (GAINVALUES[this->gain_als_] * RESOLUTIONVALUE[this->res_als_])) * this->wfac_; + float lux = ((0.6f * als) / (GAINVALUES[this->gain_als_] * RESOLUTIONVALUE[this->res_als_])) * this->wfac_; this->light_sensor_->publish_state(lux); } diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index 9cba06e483..afdc271167 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -500,12 +500,12 @@ void LTRAlsPs501Component::apply_lux_calculation_(AlsReadings &data) { // method from // https://github.com/fards/Ainol_fire_kernel/blob/83832cf8a3082fd8e963230f4b1984479d1f1a84/customer/drivers/lightsensor/ltr501als.c#L295 - if (ratio < 0.45) { - lux = 1.7743 * ch0 + 1.1059 * ch1; - } else if (ratio < 0.64) { - lux = 3.7725 * ch0 - 1.3363 * ch1; - } else if (ratio < 0.85) { - lux = 1.6903 * ch0 - 0.1693 * ch1; + if (ratio < 0.45f) { + lux = 1.7743f * ch0 + 1.1059f * ch1; + } else if (ratio < 0.64f) { + lux = 3.7725f * ch0 - 1.3363f * ch1; + } else if (ratio < 0.85f) { + lux = 1.6903f * ch0 - 0.1693f * ch1; } else { ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio"); lux = 0.0f; diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index b7fad2e876..0d43aac20e 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -480,12 +480,12 @@ void LTRAlsPsComponent::apply_lux_calculation_(AlsReadings &data) { float inv_pfactor = this->glass_attenuation_factor_; float lux = 0.0f; - if (ratio < 0.45) { - lux = (1.7743 * ch0 + 1.1059 * ch1); - } else if (ratio < 0.64 && ratio >= 0.45) { - lux = (4.2785 * ch0 - 1.9548 * ch1); - } else if (ratio < 0.85 && ratio >= 0.64) { - lux = (0.5926 * ch0 + 0.1185 * ch1); + if (ratio < 0.45f) { + lux = (1.7743f * ch0 + 1.1059f * ch1); + } else if (ratio < 0.64f && ratio >= 0.45f) { + lux = (4.2785f * ch0 - 1.9548f * ch1); + } else if (ratio < 0.85f && ratio >= 0.64f) { + lux = (0.5926f * ch0 + 0.1185f * ch1); } else { ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio"); lux = 0.0f; diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index b59bac7ebf..8776bb5558 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -23,7 +23,7 @@ void MAX17043Component::update() { if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) { this->status_set_warning(LOG_STR("Unable to read MAX17043_VCELL")); } else { - float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0; + float voltage = (1.25f * (float) (raw_voltage >> 4)) / 1000.0f; this->voltage_sensor_->publish_state(voltage); this->status_clear_warning(); } diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp index 5351d6a2cb..33abbe847a 100644 --- a/esphome/components/mcp3204/mcp3204.cpp +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -31,7 +31,7 @@ float MCP3204::read_data(uint8_t pin, bool differential) { this->disable(); uint16_t digital_value = encode_uint16(b0, b1) >> 4; - return float(digital_value) / 4096.000 * this->reference_voltage_; // in V + return float(digital_value) / 4096.000f * this->reference_voltage_; // in V } } // namespace esphome::mcp3204 diff --git a/esphome/components/mcp4461/output/mcp4461_output.cpp b/esphome/components/mcp4461/output/mcp4461_output.cpp index 6912ad5f36..3892372cab 100644 --- a/esphome/components/mcp4461/output/mcp4461_output.cpp +++ b/esphome/components/mcp4461/output/mcp4461_output.cpp @@ -29,7 +29,9 @@ void Mcp4461Wiper::write_state(float state) { } } -float Mcp4461Wiper::read_state() { return (static_cast(this->parent_->get_wiper_level_(this->wiper_)) / 256.0); } +float Mcp4461Wiper::read_state() { + return (static_cast(this->parent_->get_wiper_level_(this->wiper_)) / 256.0f); +} float Mcp4461Wiper::update_state() { this->state_ = this->read_state(); diff --git a/esphome/components/mcp4725/mcp4725.cpp b/esphome/components/mcp4725/mcp4725.cpp index a32527c725..8e94623de3 100644 --- a/esphome/components/mcp4725/mcp4725.cpp +++ b/esphome/components/mcp4725/mcp4725.cpp @@ -24,7 +24,8 @@ void MCP4725::dump_config() { // https://learn.sparkfun.com/tutorials/mcp4725-digital-to-analog-converter-hookup-guide?_ga=2.176055202.1402343014.1607953301-893095255.1606753886 void MCP4725::write_state(float state) { - const uint16_t value = (uint16_t) round(state * (pow(2, MCP4725_RES) - 1)); + constexpr uint16_t max_value = (1U << MCP4725_RES) - 1; + const uint16_t value = (uint16_t) roundf(state * max_value); this->write_byte_16(64, value << 4); } diff --git a/esphome/components/mcp4725/mcp4725.h b/esphome/components/mcp4725/mcp4725.h index 4f1f128e52..a0838dc33e 100644 --- a/esphome/components/mcp4725/mcp4725.h +++ b/esphome/components/mcp4725/mcp4725.h @@ -4,10 +4,11 @@ #include "esphome/core/component.h" #include "esphome/components/i2c/i2c.h" -static const uint8_t MCP4725_ADDR = 0x60; -static const uint8_t MCP4725_RES = 12; - namespace esphome::mcp4725 { + +static constexpr uint8_t MCP4725_ADDR = 0x60; +static constexpr uint8_t MCP4725_RES = 12; + class MCP4725 final : public Component, public output::FloatOutput, public i2c::I2CDevice { public: void setup() override; diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index d99d4fd772..14a73bc15f 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -71,10 +71,10 @@ void MICS4514Component::update() { float co = 0.0f; if (red_f > 3.4f) { co = 0.0; - } else if (red_f < 0.01) { + } else if (red_f < 0.01f) { co = 1000.0; } else { - co = 4.2 / pow(red_f, 1.2); + co = 4.2f / powf(red_f, 1.2f); } this->carbon_monoxide_sensor_->publish_state(co); } @@ -84,47 +84,47 @@ void MICS4514Component::update() { if (ox_f < 0.3f) { nitrogendioxide = 0.0; } else { - nitrogendioxide = 0.164 * pow(ox_f, 0.975); + nitrogendioxide = 0.164f * powf(ox_f, 0.975f); } this->nitrogen_dioxide_sensor_->publish_state(nitrogendioxide); } if (this->methane_sensor_ != nullptr) { float methane = 0.0f; - if (red_f > 0.9f || red_f < 0.5) { // outside the range->unlikely + if (red_f > 0.9f || red_f < 0.5f) { // outside the range->unlikely methane = 0.0; } else { - methane = 630 / pow(red_f, 4.4); + methane = 630 / powf(red_f, 4.4f); } this->methane_sensor_->publish_state(methane); } if (this->ethanol_sensor_ != nullptr) { float ethanol = 0.0f; - if (red_f > 1.0f || red_f < 0.02) { // outside the range->unlikely + if (red_f > 1.0f || red_f < 0.02f) { // outside the range->unlikely ethanol = 0.0; } else { - ethanol = 1.52 / pow(red_f, 1.55); + ethanol = 1.52f / powf(red_f, 1.55f); } this->ethanol_sensor_->publish_state(ethanol); } if (this->hydrogen_sensor_ != nullptr) { float hydrogen = 0.0f; - if (red_f > 0.9f || red_f < 0.02) { // outside the range->unlikely + if (red_f > 0.9f || red_f < 0.02f) { // outside the range->unlikely hydrogen = 0.0; } else { - hydrogen = 0.85 / pow(red_f, 1.75); + hydrogen = 0.85f / powf(red_f, 1.75f); } this->hydrogen_sensor_->publish_state(hydrogen); } if (this->ammonia_sensor_ != nullptr) { float ammonia = 0.0f; - if (red_f > 0.98f || red_f < 0.2532) { // outside the ammonia range->unlikely + if (red_f > 0.98f || red_f < 0.2532f) { // outside the ammonia range->unlikely ammonia = 0.0; } else { - ammonia = 0.9 / pow(red_f, 4.6); + ammonia = 0.9f / powf(red_f, 4.6f); } this->ammonia_sensor_->publish_state(ammonia); } diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index 79c580c6b7..15e715e675 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -1,6 +1,8 @@ #include "mmc5603.h" #include "esphome/core/log.h" +#include + namespace esphome::mmc5603 { static const char *const TAG = "mmc5603"; @@ -143,7 +145,7 @@ void MMC5603Component::update() { const float z = 0.00625 * (raw_z - 524288); - const float heading = atan2f(0.0f - x, y) * 180.0f / M_PI; + const float heading = atan2f(0.0f - x, y) * 180.0f / std::numbers::pi_v; ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f°", x, y, z, heading); if (this->x_sensor_ != nullptr) diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 492dfcaafe..cf1d409393 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -124,7 +124,6 @@ async def register_modbus_client_device(var, config): async def register_modbus_server_device(var, config): parent = await cg.get_variable(config[CONF_MODBUS_ID]) - cg.add(var.set_parent(parent)) cg.add(var.set_address(config[CONF_ADDRESS])) cg.add(parent.register_device(var)) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index c9ba2e837e..488bcf1459 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -92,14 +92,10 @@ int32_t Modbus::tx_delay_remaining() { int32_t ModbusClientHub::tx_delay_remaining() { const uint32_t now = millis(); - // Turnaround delay only applies after a broadcast: no response is expected, so we must give listening devices - // quiet time to process it before the next request. For normal unicast request/response the received reply already - // provides the inter-frame timing, so adding turnaround there just throttles throughput. - const uint16_t turnaround = this->last_send_was_broadcast_ ? this->turnaround_delay_ms_ : 0; - return std::max( - {(int32_t) 0, - (int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + turnaround - (now - this->last_send_)), - (int32_t) (this->frame_delay_ms_ + turnaround - (now - this->last_modbus_byte_))}); + return std::max({(int32_t) 0, + (int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + this->turnaround_delay_ms_ - + (now - this->last_send_)), + (int32_t) (this->frame_delay_ms_ + this->turnaround_delay_ms_ - (now - this->last_modbus_byte_))}); } bool Modbus::tx_blocked() { @@ -258,7 +254,7 @@ bool ModbusServerHub::parse_modbus_client_frame_() { std::memcpy(data, this->rx_buffer_.data() + data_offset, data_len); this->clear_rx_buffer_(LOG_STR("parse succeeded"), false, frame_length); - this->process_modbus_client_frame_(address, function_code, data, data_len); + this->process_modbus_client_frame_(address, function_code, data); return true; } @@ -321,10 +317,8 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, uint8_t funct } void ModbusServerHub::process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *, uint16_t) { - for (auto *device : this->devices_) { - if (device->address_ == address) { - ESP_LOGE(TAG, "Unexpected response from address %" PRIu8 ", which is mapped to this device.", address); - } + if (this->find_device_(address) != nullptr) { + ESP_LOGE(TAG, "Unexpected response from address %" PRIu8 ", which is mapped to this device.", address); } if (this->expecting_peer_response_ == address) { @@ -338,31 +332,124 @@ void ModbusServerHub::process_modbus_server_frame(uint8_t address, uint8_t funct this->expecting_peer_response_ = 0; } -void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data, - uint16_t len) { - bool found = false; - +ModbusServerDevice *ModbusServerHub::find_device_(uint8_t address) { for (auto *device : this->devices_) { - if (device->address_ == address) { - found = true; - - if (static_cast(function_code) == ModbusFunctionCode::READ_HOLDING_REGISTERS || - static_cast(function_code) == ModbusFunctionCode::READ_INPUT_REGISTERS) { - device->on_modbus_read_registers(function_code, helpers::get_data(data, 0), - helpers::get_data(data, 2)); - } else if (static_cast(function_code) == ModbusFunctionCode::WRITE_SINGLE_REGISTER || - static_cast(function_code) == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { - device->on_modbus_write_registers(function_code, std::vector(data, data + len)); - } else { - ESP_LOGW(TAG, "Unsupported function code %" PRIu8, function_code); - device->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION); - } + if (device->get_address() == address) { + return device; } } + return nullptr; +} - if (!found) { +bool ModbusServerHub::check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address, + uint16_t number_of_registers) { + if ((uint32_t) start_address + number_of_registers > 0x10000u) { + ESP_LOGW(TAG, "Register address out of range - start: %" PRIu16 " num: %" PRIu16, start_address, + number_of_registers); + this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); + return false; + } + return true; +} + +void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data) { + ModbusServerDevice *device = this->find_device_(address); + if (device == nullptr) { this->expecting_peer_response_ = address; ESP_LOGV(TAG, "Request to peer %" PRIu8 " received", address); + return; + } + + ServerResponseStatus status; + uint8_t response_buffer[modbus::MAX_RAW_SIZE]; + const uint8_t *response_data = response_buffer; + uint16_t response_len = 0; + + switch (static_cast(function_code)) { + case ModbusFunctionCode::READ_HOLDING_REGISTERS: + case ModbusFunctionCode::READ_INPUT_REGISTERS: { + // PDU data: start address(2) + quantity(2). + uint16_t start_address = helpers::get_data(data, 0); + uint16_t number_of_registers = helpers::get_data(data, 2); + if (number_of_registers == 0 || number_of_registers > MAX_NUM_OF_REGISTERS_TO_READ) { + ESP_LOGW(TAG, "Invalid number of registers %" PRIu16, number_of_registers); + this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } + if (!this->check_register_range_(address, function_code, start_address, number_of_registers)) { + return; + } + RegisterValues registers; + if (static_cast(function_code) == ModbusFunctionCode::READ_HOLDING_REGISTERS) { + status = device->on_modbus_read_holding_registers(start_address, number_of_registers, registers); + } else { + status = device->on_modbus_read_input_registers(start_address, number_of_registers, registers); + } + + // A handler that returns an exception leaves registers partially filled, so check the exception + // first and forward it before validating the register count on the success path. + if (status.has_value()) { + this->send_exception_(address, function_code, status.value()); + return; + } + + if (registers.size() != number_of_registers) { + ESP_LOGE(TAG, "Incorrect response %" PRIu16 " requested, %zu returned", number_of_registers, registers.size()); + this->send_exception_(address, function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE); + return; + } + + response_buffer[response_len++] = static_cast(number_of_registers * 2); // actual byte count + for (auto r : registers) { + auto register_bytes = decode_value(r); + response_buffer[response_len++] = register_bytes[0]; + response_buffer[response_len++] = register_bytes[1]; + } + break; + } + case ModbusFunctionCode::WRITE_SINGLE_REGISTER: + case ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS: { + // PDU data: start address(2) [+ quantity(2) + byte count(1)] + register values. + // A single-register write always targets one register; for a multiple-register write the + // quantity is in the frame and its byte count must equal quantity * 2. The register values are + // assembled into registers below so the handler doesn't have to know the request framing. + uint16_t start_address = helpers::get_data(data, 0); + uint16_t number_of_registers = 1; + uint16_t values_offset = 2; // single write: values follow the 2-byte start address + if (static_cast(function_code) == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { + number_of_registers = helpers::get_data(data, 2); + uint8_t number_of_bytes = helpers::get_data(data, 4); + values_offset = 5; // multiple write: values follow start address(2) + quantity(2) + byte count(1) + if (number_of_registers == 0 || number_of_registers > MAX_NUM_OF_REGISTERS_TO_WRITE || + number_of_registers * 2 != number_of_bytes) { + ESP_LOGW(TAG, "Invalid number of registers %" PRIu16 " or bytes %" PRIu8, number_of_registers, + number_of_bytes); + this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE); + return; + } + if (!this->check_register_range_(address, function_code, start_address, number_of_registers)) { + return; + } + } + // Assemble the register values (host byte order) so the handler never sees wire framing. + RegisterValues registers; + for (uint16_t i = 0; i < number_of_registers; i++) { + registers.push_back(helpers::get_data(data, values_offset + i * 2)); + } + status = device->on_modbus_write_registers(start_address, registers); + response_data = data; // echo the request header per Modbus 6.6, 6.12 + response_len = 4; + break; + } + default: + ESP_LOGW(TAG, "Unsupported function code %" PRIu8, function_code); + this->send_exception_(address, function_code, ModbusExceptionCode::ILLEGAL_FUNCTION); + return; + } + if (status.has_value()) { + this->send_exception_(address, function_code, status.value()); + } else { + this->send_response_(address, function_code, response_data, response_len); } } @@ -400,7 +487,6 @@ bool Modbus::send_frame_(const ModbusFrame &frame) { format_hex_pretty_to(hex_buf, frame.data.get(), frame.size), now - this->last_send_, now - this->last_modbus_byte_); this->last_send_ = now; - this->last_send_was_broadcast_ = frame.size > 0 && frame.data[0] == 0; return true; } @@ -416,8 +502,7 @@ void ModbusClientHub::send_next_frame_() { ModbusDeviceCommand &command = this->tx_buffer_.front(); if (this->send_frame_(command.frame)) { - if (!this->last_send_was_broadcast_) - this->waiting_for_response_ = std::move(command); + this->waiting_for_response_ = std::move(command); } else { if (command.device) command.device->on_modbus_not_sent(); @@ -455,17 +540,27 @@ float Modbus::get_setup_priority() const { return setup_priority::BUS - 1.0f; } -void ModbusServerHub::send(uint8_t address, uint8_t function_code, const std::vector &payload) { - const uint16_t len = static_cast(2 + payload.size()); - if (len > MAX_RAW_SIZE) { - ESP_LOGE(TAG, "Server send frame too large (%" PRIu16 " bytes)", len); +void ModbusServerHub::send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, + uint16_t payload_len) { + // Build the raw frame (address + function code + payload) in a stack buffer; it's consumed + // immediately by send_raw_ and a full raw frame never exceeds MAX_RAW_SIZE. + if (payload_len + 2 > MAX_RAW_SIZE) { + ESP_LOGE(TAG, "Server response too large (%" PRIu16 " bytes)", static_cast(payload_len + 2)); return; } uint8_t raw_frame[MAX_RAW_SIZE]; raw_frame[0] = address; raw_frame[1] = function_code; - std::memcpy(raw_frame + 2, payload.data(), payload.size()); - this->send_raw_(raw_frame, len); + std::memcpy(raw_frame + 2, payload, payload_len); + this->send_raw_(raw_frame, payload_len + 2); +} + +void ModbusServerHub::send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code) { + uint8_t raw_frame[3]; + raw_frame[0] = address; + raw_frame[1] = function_code | FUNCTION_CODE_EXCEPTION_MASK; + raw_frame[2] = static_cast(exception_code); + this->send_raw_(raw_frame, 3); } // Raw send for client: pushes to tx queue. Everything except the CRC must be contained in payload. diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index da0db13a07..4aa3a16c3a 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -63,7 +63,6 @@ class Modbus : public uart::UARTDevice, public Component { uint32_t last_receive_check_{0}; uint32_t last_send_{0}; uint32_t last_send_tx_offset_{0}; - bool last_send_was_broadcast_{false}; uint16_t frame_delay_ms_{5}; uint16_t long_rx_buffer_delay_ms_{0}; @@ -130,22 +129,22 @@ class ModbusServerHub : public Modbus { public: ModbusServerHub() = default; void dump_config() override; - void send(uint8_t address, uint8_t function_code, const std::vector &payload); - ESPDEPRECATED("Use ModbusServerDevice::send_raw instead. Removed in 2026.10.0", "2026.4.0") - void send_raw(const std::vector &payload) { - this->send_raw_(payload.data(), static_cast(payload.size())); - }; void register_device(ModbusServerDevice *device) { this->devices_.push_back(device); } protected: - friend class ModbusServerDevice; - void parse_modbus_frames() override; bool parse_modbus_client_frame_(); // Parsers need to handle standard (ModbusFunctionCode) and custom (uint8_t) function codes, so we use uint8_t here. void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override; - void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len); + void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data); + ModbusServerDevice *find_device_(uint8_t address); + // Returns true if [start_address, start_address + number_of_registers) fits in the 16-bit address space. + // On failure, logs and sends an ILLEGAL_DATA_ADDRESS exception to the client. + bool check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address, + uint16_t number_of_registers); void send_raw_(const uint8_t *payload, uint16_t len); + void send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code); + void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len); uint8_t expecting_peer_response_{0}; std::vector devices_; @@ -200,35 +199,41 @@ class ModbusClientDevice { // This is for compatibility with external components using the former class name using ModbusDevice = ModbusClientDevice; +// Result of a server register handler: std::nullopt means success, otherwise the Modbus exception code to return. +using ServerResponseStatus = std::optional; +// Register values exchanged with server handlers, in host byte order. Sized at the larger of the two protocol +// maxima (read = 125 / 0x7D, write = 123 / 0x7B); the per-direction count limit is enforced by the hub, not by +// the capacity of this type. +using RegisterValues = StaticVector; + class ModbusServerDevice { public: - ModbusServerDevice() = default; - ModbusServerDevice(ModbusServerHub *parent, uint8_t address) : parent_(parent), address_(address) {} virtual ~ModbusServerDevice() = default; + ModbusServerDevice() = default; + // Polymorphic base: non-copyable and non-movable to prevent slicing (Rule of Five). ModbusServerDevice(const ModbusServerDevice &) = delete; ModbusServerDevice &operator=(const ModbusServerDevice &) = delete; ModbusServerDevice(ModbusServerDevice &&) = delete; ModbusServerDevice &operator=(ModbusServerDevice &&) = delete; - void set_parent(ModbusServerHub *parent) { this->parent_ = parent; } void set_address(uint8_t address) { this->address_ = address; } - virtual void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers){}; - virtual void on_modbus_write_registers(uint8_t function_code, const std::vector &data){}; - void send(uint8_t function, const std::vector &payload) { - this->parent_->send(this->address_, function, payload); - } - void send_raw(const std::vector &payload) { - this->parent_->send_raw_(payload.data(), static_cast(payload.size())); - } - void send_error(uint8_t function_code, ModbusExceptionCode exception_code) { - uint8_t error_response[3] = {this->address_, uint8_t(function_code | FUNCTION_CODE_EXCEPTION_MASK), - static_cast(exception_code)}; - this->parent_->send_raw_(error_response, 3); - } + uint8_t get_address() const { return this->address_; } + virtual ServerResponseStatus on_modbus_read_registers(uint16_t start_address, uint16_t number_of_registers, + RegisterValues ®isters) { + return ModbusExceptionCode::ILLEGAL_FUNCTION; + }; + virtual ServerResponseStatus on_modbus_read_input_registers(uint16_t start_address, uint16_t number_of_registers, + RegisterValues ®isters) { + return this->on_modbus_read_registers(start_address, number_of_registers, registers); + }; + virtual ServerResponseStatus on_modbus_read_holding_registers(uint16_t start_address, uint16_t number_of_registers, + RegisterValues ®isters) { + return this->on_modbus_read_registers(start_address, number_of_registers, registers); + }; + virtual ServerResponseStatus on_modbus_write_registers(uint16_t start_address, const RegisterValues ®isters) { + return ModbusExceptionCode::ILLEGAL_FUNCTION; + }; protected: - friend ModbusServerHub; - - ModbusServerHub *parent_{nullptr}; uint8_t address_{0}; }; diff --git a/esphome/components/modbus/modbus_definitions.h b/esphome/components/modbus/modbus_definitions.h index 49172b9dca..1c03498f1d 100644 --- a/esphome/components/modbus/modbus_definitions.h +++ b/esphome/components/modbus/modbus_definitions.h @@ -82,7 +82,7 @@ static constexpr uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D // Smallest possible frame is 4 bytes (custom function with no data): address(1) + function(1) + CRC(2) static constexpr uint16_t MIN_FRAME_SIZE = 4; static constexpr uint16_t MAX_PDU_SIZE = 253; // Max PDU size is 256 - address(1) - CRC(2) = 253 -static constexpr uint16_t MAX_RAW_SIZE = 254; // Max RAW size is 255 - CRC(2) = 254 +static constexpr uint16_t MAX_RAW_SIZE = 254; // Max RAW size is 256 - CRC(2) = 254 static constexpr uint16_t MAX_FRAME_SIZE = 256; /// End of Modbus definitions } // namespace esphome::modbus diff --git a/esphome/components/modbus/modbus_helpers.cpp b/esphome/components/modbus/modbus_helpers.cpp index 4cddfca104..53fa6afacb 100644 --- a/esphome/components/modbus/modbus_helpers.cpp +++ b/esphome/components/modbus/modbus_helpers.cpp @@ -101,53 +101,19 @@ static size_t required_payload_size(SensorValueType sensor_value_type) { } } -void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type) { - switch (value_type) { - case SensorValueType::U_WORD: - case SensorValueType::S_WORD: - data.push_back(value & 0xFFFF); - break; - case SensorValueType::U_DWORD: - case SensorValueType::S_DWORD: - case SensorValueType::FP32: - data.push_back((value & 0xFFFF0000) >> 16); - data.push_back(value & 0xFFFF); - break; - case SensorValueType::U_DWORD_R: - case SensorValueType::S_DWORD_R: - case SensorValueType::FP32_R: - data.push_back(value & 0xFFFF); - data.push_back((value & 0xFFFF0000) >> 16); - break; - case SensorValueType::U_QWORD: - case SensorValueType::S_QWORD: - data.push_back((value & 0xFFFF000000000000) >> 48); - data.push_back((value & 0xFFFF00000000) >> 32); - data.push_back((value & 0xFFFF0000) >> 16); - data.push_back(value & 0xFFFF); - break; - case SensorValueType::U_QWORD_R: - case SensorValueType::S_QWORD_R: - data.push_back(value & 0xFFFF); - data.push_back((value & 0xFFFF0000) >> 16); - data.push_back((value & 0xFFFF00000000) >> 32); - data.push_back((value & 0xFFFF000000000000) >> 48); - break; - default: - ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast(value_type)); - break; - } +void log_unsupported_value_type(SensorValueType value_type) { + ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast(value_type)); } -int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, +int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset, uint32_t bitmask, bool *error_return) { int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits // Validate offset against the buffer for all types, including RAW/unsupported, so // a malformed or misconfigured frame still produces an error log. - if (static_cast(offset) > data.size()) { + if (static_cast(offset) > size) { ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu", static_cast(sensor_value_type), - static_cast(offset), data.size()); + static_cast(offset), size); if (error_return) *error_return = true; return value; @@ -158,10 +124,9 @@ int64_t payload_to_number(const std::vector &data, SensorValueType sens return value; } - if (data.size() - offset < required_size) { + if (size - offset < required_size) { ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu required=%zu", - static_cast(sensor_value_type), static_cast(offset), data.size(), - required_size); + static_cast(sensor_value_type), static_cast(offset), size, required_size); if (error_return) *error_return = true; return value; @@ -214,6 +179,31 @@ int64_t payload_to_number(const std::vector &data, SensorValueType sens return value; } +int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type, + bool *error_return) { + const size_t required_size = required_payload_size(sensor_value_type); + if (required_size == 0) { + return 0; // RAW/unsupported: nothing to read + } + const size_t required_words = required_size / 2; + if (required_words > count) { + ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%zu", + static_cast(sensor_value_type), count, required_words); + if (error_return) + *error_return = true; + return 0; + } + // Serialize the needed words back to big-endian bytes and reuse the audited byte decoder so the + // sign-extension behaviour stays identical to the wire path. + uint8_t bytes[8]; // at most 4 registers (QWORD) + for (size_t i = 0; i < required_words; i++) { + uint16_t reg = registers[i]; + bytes[i * 2] = static_cast(reg >> 8); + bytes[i * 2 + 1] = static_cast(reg & 0xFF); + } + return payload_to_number(bytes, required_size, sensor_value_type, 0, 0xFFFFFFFF, error_return); +} + StaticVector create_client_pdu(ModbusFunctionCode function_code, uint16_t start_address, uint16_t number_of_entities, const uint8_t *values, size_t values_len) { diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h index b637d872cf..b7b9020945 100644 --- a/esphome/components/modbus/modbus_helpers.h +++ b/esphome/components/modbus/modbus_helpers.h @@ -224,24 +224,77 @@ template N mask_and_shift_by_rightbit(N data, uint32_t mask) { return 0; } -/** Convert float value to vector suitable for sending - * @param data target for payload - * @param value float value to convert - * @param value_type defines if 16/32 or FP32 is used - * @return vector containing the modbus register words in correct order - */ -void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type); +// Logs an error for an unsupported value type. Defined in the .cpp so logging stays out of headers. +void log_unsupported_value_type(SensorValueType value_type); -/** Convert vector response payload to number. +/** Append the Modbus register words for value to data. + * Works with any container exposing push_back(uint16_t) (e.g. std::vector or StaticVector). + */ +template void number_to_payload(Container &data, int64_t value, SensorValueType value_type) { + switch (value_type) { + case SensorValueType::U_WORD: + case SensorValueType::S_WORD: + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_DWORD: + case SensorValueType::S_DWORD: + case SensorValueType::FP32: + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_DWORD_R: + case SensorValueType::S_DWORD_R: + case SensorValueType::FP32_R: + data.push_back(value & 0xFFFF); + data.push_back((value & 0xFFFF0000) >> 16); + break; + case SensorValueType::U_QWORD: + case SensorValueType::S_QWORD: + data.push_back((value & 0xFFFF000000000000) >> 48); + data.push_back((value & 0xFFFF00000000) >> 32); + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD_R: + data.push_back(value & 0xFFFF); + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back((value & 0xFFFF00000000) >> 32); + data.push_back((value & 0xFFFF000000000000) >> 48); + break; + default: + log_unsupported_value_type(value_type); + break; + } +} + +/** Convert a raw response payload to a number. * @param data payload with the data to convert + * @param size number of bytes available in data * @param sensor_value_type defines if 16/32/64 bits or FP32 is used * @param offset offset to the data in data * @param bitmask bitmask used for masking and shifting * @return 64-bit number of the payload */ -int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, +int64_t payload_to_number(const uint8_t *data, size_t size, SensorValueType sensor_value_type, uint8_t offset, uint32_t bitmask, bool *error_return = nullptr); +/** Convert vector response payload to number. */ +inline int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask, bool *error_return = nullptr) { + return payload_to_number(data.data(), data.size(), sensor_value_type, offset, bitmask, error_return); +} + +/** Reconstruct a number from register words (host byte order). Inverse of number_to_payload. + * Decodes the value at the start of the given span; advance the pointer to read successive values. + * @param registers register values in host byte order + * @param count number of registers available in registers + * @param sensor_value_type defines if 16/32/64 bits or FP32 is used + * @return 64-bit number of the registers + */ +int64_t registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type, + bool *error_return = nullptr); + /** Create a modbus clinet pdu for reading/writing single/multiple coils/register/inputs. * @param function_code the modbus function code to use. One of: * READ_COILS diff --git a/esphome/components/modbus_server/modbus_server.cpp b/esphome/components/modbus_server/modbus_server.cpp index c294d08888..bb264eb993 100644 --- a/esphome/components/modbus_server/modbus_server.cpp +++ b/esphome/components/modbus_server/modbus_server.cpp @@ -3,26 +3,18 @@ #include "esphome/core/log.h" namespace esphome::modbus_server { -using modbus::ModbusFunctionCode; using modbus::ModbusExceptionCode; -using modbus::helpers::payload_to_number; +using modbus::helpers::registers_to_number; 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) { +modbus::ServerResponseStatus ModbusServer::on_modbus_read_registers(uint16_t start_address, + uint16_t number_of_registers, + modbus::RegisterValues ®isters) { ESP_LOGV(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); + "Received read holding/input registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%X.", + this->address_, 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 %" PRIu16 ". Sending exception response.", number_of_registers); - this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); - return; - } - - std::vector 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_) { @@ -36,10 +28,7 @@ void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t star server_register->address, static_cast(server_register->value_type), server_register->register_count, server_register->format_value(value, value_buf, sizeof(value_buf))); - std::vector 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()); + modbus::helpers::number_to_payload(registers, value, server_register->value_type); current_address += server_register->register_count; found = true; break; @@ -53,92 +42,37 @@ void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t star "Could not match any register to address 0x%02X, but default allowed. " "Returning default value: %" PRIu16 ".", current_address, this->server_courtesy_response_.register_value); - sixteen_bit_response.push_back(this->server_courtesy_response_.register_value); + registers.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; + return ModbusExceptionCode::ILLEGAL_DATA_ADDRESS; } } } - std::vector response; - if (number_of_registers != sixteen_bit_response.size()) - ESP_LOGW(TAG, "Response size not matched to request register count."); - response.push_back(sixteen_bit_response.size() * 2); // actual byte count - 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, response); + return {}; } -void ModbusServer::on_modbus_write_registers(uint8_t function_code, const std::vector &data) { - uint16_t number_of_registers; - uint16_t payload_offset; +modbus::ServerResponseStatus ModbusServer::on_modbus_write_registers(uint16_t start_address, + const modbus::RegisterValues ®isters) { + // registers holds the values to write in host byte order; its size is the register count. + ESP_LOGV(TAG, "Received write registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%zX.", + this->address_, start_address, registers.size()); - if (static_cast(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 %" PRIu16 ". 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 %" PRIu16 " bytes is not 2 times the number of registers (%" PRIu16 - "). 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 (static_cast(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 &callback) -> bool { - uint16_t offset = payload_offset; - for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { + auto for_each_register = + [this, start_address, + ®isters](const std::function &callback) -> bool { + uint16_t register_offset = 0; + for (uint32_t current_address = start_address; current_address < start_address + registers.size();) { bool ok = false; for (auto *server_register : this->server_registers_) { if (server_register->address == current_address) { - ok = callback(server_register, offset); + ok = callback(server_register, register_offset); current_address += server_register->register_count; - offset += server_register->register_count * sizeof(uint16_t); + register_offset += server_register->register_count; break; } } @@ -150,36 +84,41 @@ void ModbusServer::on_modbus_write_registers(uint8_t function_code, const std::v 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; - })) { - ESP_LOGW(TAG, "Invalid register address. Sending exception response."); - this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); - return; - } - - // Actually write to the registers: - if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) { - bool error = false; - int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF, &error); - if (error) { - return false; - } else { - return server_register->write_lambda(number); + // Pre-flight: every targeted register must be writable AND have its full value present in the request, + // so we never apply a partial write before discovering a problem. The commit pass below re-runs + // registers_to_number rather than caching the decoded values: using the same function for the check and + // the write keeps a single source of truth for the decode bound, independent of how register_count was set. + ModbusExceptionCode precheck = ModbusExceptionCode::ILLEGAL_DATA_ADDRESS; // unmatched or unwritable register + if (!for_each_register([&precheck, ®isters](ServerRegister *server_register, uint16_t register_offset) -> bool { + if (server_register->write_lambda == nullptr) { + return false; // unwritable -> ILLEGAL_DATA_ADDRESS } + bool error = false; + registers_to_number(registers.data() + register_offset, registers.size() - register_offset, + server_register->value_type, &error); + if (error) { + precheck = ModbusExceptionCode::ILLEGAL_DATA_VALUE; // request doesn't supply the full value + return false; + } + return true; })) { - ESP_LOGW(TAG, "Could not write all registers. Sending exception response."); - this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE); - return; + ESP_LOGW(TAG, "Write request rejected before applying any register. Sending exception response."); + return precheck; } - std::vector 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); + // Commit: every value is known writable and decodable, so the only failure now is a user write callback + // rejecting the value at runtime -- which cannot be rolled back. + if (!for_each_register([®isters](ServerRegister *server_register, uint16_t register_offset) { + int64_t number = registers_to_number(registers.data() + register_offset, registers.size() - register_offset, + server_register->value_type); + return server_register->write_lambda(number); + })) { + ESP_LOGW(TAG, "A register write callback failed mid-sequence; earlier writes were already applied."); + return ModbusExceptionCode::SERVICE_DEVICE_FAILURE; + } + + // Success: the caller builds the write response (an echo of the request header). + return {}; } void ModbusServer::dump_config() { diff --git a/esphome/components/modbus_server/modbus_server.h b/esphome/components/modbus_server/modbus_server.h index fa1376542c..0c22454528 100644 --- a/esphome/components/modbus_server/modbus_server.h +++ b/esphome/components/modbus_server/modbus_server.h @@ -98,9 +98,11 @@ class ModbusServer : public Component, public modbus::ModbusServerDevice { /// 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; + modbus::ServerResponseStatus on_modbus_read_registers(uint16_t start_address, uint16_t number_of_registers, + modbus::RegisterValues ®isters) 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 &data) final; + modbus::ServerResponseStatus on_modbus_write_registers(uint16_t start_address, + const modbus::RegisterValues ®isters) 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; diff --git a/esphome/components/mpl3115a2/mpl3115a2.cpp b/esphome/components/mpl3115a2/mpl3115a2.cpp index d7994327b1..238e37aff0 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.cpp +++ b/esphome/components/mpl3115a2/mpl3115a2.cpp @@ -75,16 +75,16 @@ void MPL3115A2Component::update() { float altitude = 0, pressure = 0; if (this->altitude_ != nullptr) { int32_t alt = encode_uint32(buffer[0], buffer[1], buffer[2], 0); - altitude = float(alt) / 65536.0; + altitude = float(alt) / 65536.0f; this->altitude_->publish_state(altitude); } else { uint32_t p = encode_uint32(0, buffer[0], buffer[1], buffer[2]); - pressure = float(p) / 6400.0; + pressure = float(p) / 6400.0f; if (this->pressure_ != nullptr) this->pressure_->publish_state(pressure); } int16_t t = encode_uint16(buffer[3], buffer[4]); - float temperature = float(t) / 256.0; + float temperature = float(t) / 256.0f; if (this->temperature_ != nullptr) this->temperature_->publish_state(temperature); diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 443c983efe..d5ee4c6a9b 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -115,9 +115,9 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // max_temp root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature(); // target_temp_step - root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1; + root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1f; // current_temp_step - root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1; + root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1f; // temperature units are always coerced to Celsius internally root[MQTT_TEMPERATURE_UNIT] = "C"; diff --git a/esphome/components/msa3xx/msa3xx.cpp b/esphome/components/msa3xx/msa3xx.cpp index f23fcfc8ea..ecde0cb117 100644 --- a/esphome/components/msa3xx/msa3xx.cpp +++ b/esphome/components/msa3xx/msa3xx.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "msa3xx"; const uint8_t MSA_3XX_PART_ID = 0x13; const float GRAVITY_EARTH = 9.80665f; -const float LSB_COEFF = 1000.0f / (GRAVITY_EARTH * 3.9); // LSB to 1 LSB = 3.9mg = 0.0039g +const float LSB_COEFF = 1000.0f / (GRAVITY_EARTH * 3.9f); // LSB to 1 LSB = 3.9mg = 0.0039g const float G_OFFSET_MIN = -4.5f; // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe const float G_OFFSET_MAX = 4.5f; // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index a332d342ee..a356d54e2f 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -176,7 +176,7 @@ void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_pr void Nextion::goto_page(uint8_t page) { this->add_no_result_to_queue_with_printf_("page", "page %i", page); } void Nextion::set_backlight_brightness(float brightness) { - if (brightness < 0 || brightness > 1.0) { + if (brightness < 0 || brightness > 1.0f) { ESP_LOGD(TAG, "Brightness out of bounds (0-1.0)"); return; } diff --git a/esphome/components/nrf52/framework.py b/esphome/components/nrf52/framework.py index 05feadb001..7aec6b088e 100644 --- a/esphome/components/nrf52/framework.py +++ b/esphome/components/nrf52/framework.py @@ -54,22 +54,15 @@ def _get_toolchain_path(version: str) -> Path: return _get_tools_path() / "toolchains" / version -# onexc/dir_fd were added to shutil.rmtree in 3.12; the 3.11 branch uses onerror. _SITECUSTOMIZE = """\ -import os, stat, shutil, sys +import os, stat, shutil _orig = shutil.rmtree def _handler(func, path, exc): os.chmod(path, stat.S_IWRITE); func(path) -if sys.version_info >= (3, 12): - def _rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None): - if onerror is None and onexc is None: - onexc = _handler - return _orig(path, ignore_errors=ignore_errors, onerror=onerror, onexc=onexc, dir_fd=dir_fd) -else: - def _rmtree(path, ignore_errors=False, onerror=None): - if onerror is None: - onerror = _handler - return _orig(path, ignore_errors=ignore_errors, onerror=onerror) +def _rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None): + if onerror is None and onexc is None: + onexc = _handler + return _orig(path, ignore_errors=ignore_errors, onerror=onerror, onexc=onexc, dir_fd=dir_fd) shutil.rmtree = _rmtree """ diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index 1ee4c9191b..5cf7c19880 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -541,7 +541,7 @@ void OpenTherm::debug_error(OpenThermError &error) const { error.capture, error.bit_pos); } -float OpenthermData::f88() { return ((float) this->s16()) / 256.0; } +float OpenthermData::f88() { return ((float) this->s16()) / 256.0f; } void OpenthermData::f88(float value) { this->s16((int16_t) (value * 256)); } diff --git a/esphome/components/opentherm/output/opentherm_output.cpp b/esphome/components/opentherm/output/opentherm_output.cpp index 4092358d75..9b87cd8d12 100644 --- a/esphome/components/opentherm/output/opentherm_output.cpp +++ b/esphome/components/opentherm/output/opentherm_output.cpp @@ -12,8 +12,9 @@ void opentherm::OpenthermOutput::write_state(float state) { #else bool zero_means_zero = false; #endif - this->state = - state < 0.003 && zero_means_zero ? 0.0 : clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_); + this->state = state < 0.003f && zero_means_zero + ? 0.0f + : clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_); this->has_state_ = true; ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); } diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index a96941325c..eb48d8a74a 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -85,7 +85,7 @@ class OpenThreadSrpComponent final : public Component { public: void set_mdns(esphome::mdns::MDNSComponent *mdns); // This has to run after the mdns component or else no services are available to advertise - float get_setup_priority() const override { return this->mdns_->get_setup_priority() - 1.0; } + float get_setup_priority() const override { return this->mdns_->get_setup_priority() - 1.0f; } void setup() override; static void srp_callback(otError err, const otSrpClientHostInfo *host_info, const otSrpClientService *services, const otSrpClientService *removed_services, void *context); diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index 1988f574db..a7ae631956 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -1,10 +1,7 @@ #include "pid_autotuner.h" #include "esphome/core/log.h" #include - -#ifndef M_PI -#define M_PI 3.1415926535897932384626433 -#endif +#include namespace esphome::pid { @@ -126,7 +123,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce float osc_ampl = this->amplitude_detector_.get_mean_oscillation_amplitude(); float d = (this->relay_function_.output_positive - this->relay_function_.output_negative) / 2.0f; ESP_LOGVV(TAG, " Relay magnitude: %f", d); - this->ku_ = 4.0f * d / float(M_PI * osc_ampl); + this->ku_ = 4.0f * d / (std::numbers::pi_v * osc_ampl); this->pu_ = this->frequency_detector_.get_mean_oscillation_period(); this->state_ = AUTOTUNE_SUCCEEDED; @@ -300,7 +297,7 @@ bool PIDAutotuner::OscillationFrequencyDetector::is_increase_decrease_symmetrica min_interval = std::min(min_interval, interval); } float ratio = min_interval / float(max_interval); - return ratio >= 0.66; + return ratio >= 0.66f; } // ================== OscillationAmplitudeDetector ================== diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index 5b04a904b5..ba6a71f97d 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -3,6 +3,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" #include +#include namespace esphome::qmc5883l { @@ -173,7 +174,7 @@ void QMC5883LComponent::read_sensor_() { const float y = int16_t(raw[1]) * mg_per_bit * 0.1f; const float z = int16_t(raw[2]) * mg_per_bit * 0.1f; - float heading = atan2f(0.0f - x, y) * 180.0f / M_PI; + float heading = atan2f(0.0f - x, y) * 180.0f / std::numbers::pi_v; float temp = NAN; if (this->temperature_sensor_ != nullptr) { diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index bb47e7b0f5..547991f75e 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -276,7 +276,7 @@ void QMP6988Component::write_oversampling_temperature_(QMP6988Oversampling overs void QMP6988Component::calculate_altitude_(float pressure, float temp) { float altitude; - altitude = (pow((101325 / pressure), 1 / 5.257) - 1) * (temp + 273.15) / 0.0065; + altitude = (powf((101325 / pressure), 1 / 5.257f) - 1) * (temp + 273.15f) / 0.0065f; this->qmp6988_data_.altitude = altitude; } diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index c9c6a546ab..2eb76a1087 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -4,6 +4,7 @@ #include #include +#include namespace esphome::rd03d { @@ -233,7 +234,7 @@ void RD03DComponent::publish_target_(uint8_t target_num, int16_t x, int16_t y, i // Angle is measured from the Y axis (radar forward direction) if (target.angle != nullptr) { if (valid) { - float angle = std::atan2(static_cast(x), static_cast(y)) * 180.0f / M_PI; + float angle = std::atan2(static_cast(x), static_cast(y)) * 180.0f / std::numbers::pi_v; target.angle->publish_state(angle); } else { target.angle->publish_state(NAN); diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index d733394b78..12e4d14ba2 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -95,7 +95,7 @@ void RuntimeStatsCollector::log_stats_() { ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count, stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f, - stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0); + stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0f); } } diff --git a/esphome/components/servo/servo.cpp b/esphome/components/servo/servo.cpp index d2028ce9bd..8d5344cf44 100644 --- a/esphome/components/servo/servo.cpp +++ b/esphome/components/servo/servo.cpp @@ -86,7 +86,7 @@ void Servo::write(float value) { void Servo::internal_write(float value) { value = clamp(value, -1.0f, 1.0f); float level; - if (value < 0.0) { + if (value < 0.0f) { level = std::lerp(this->idle_level_, this->min_level_, -value); } else { level = std::lerp(this->idle_level_, this->max_level_, value); diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index b0f43f0ffc..b69e417591 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -207,7 +207,7 @@ bool ShellyDimmer::upgrade_firmware_() { uint16_t ShellyDimmer::convert_brightness_(float brightness) { // Special case for zero as only zero means turn off completely. - if (brightness == 0.0) { + if (brightness == 0.0f) { return 0; } diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index a93e396367..99ab7932d6 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -121,7 +121,7 @@ void SoundLevelComponent::loop() { if (this->sample_count_ == samples_in_window) { // Processed enough samples for the measurement window, compute and publish the sensor values if (this->peak_sensor_ != nullptr) { - const float peak_db = 10.0f * log10(static_cast(this->squared_peak_) / MAX_SAMPLE_SQUARED_DENOMINATOR); + const float peak_db = 10.0f * log10f(static_cast(this->squared_peak_) / MAX_SAMPLE_SQUARED_DENOMINATOR); this->peak_sensor_->publish_state(peak_db); this->squared_peak_ = 0; // reset accumulator diff --git a/esphome/components/spa06_base/spa06_base.cpp b/esphome/components/spa06_base/spa06_base.cpp index b0490628cb..d3de5168e4 100644 --- a/esphome/components/spa06_base/spa06_base.cpp +++ b/esphome/components/spa06_base/spa06_base.cpp @@ -224,7 +224,7 @@ bool SPA06Component::soft_reset_() { } // Temperature conversion formula. See datasheet pg. 14 -float SPA06Component::convert_temperature_(const float &t_raw_sc) { return this->c0_ * 0.5 + this->c1_ * t_raw_sc; } +float SPA06Component::convert_temperature_(const float &t_raw_sc) { return this->c0_ * 0.5f + this->c1_ * t_raw_sc; } // Pressure conversion formula. See datasheet pg. 14 float SPA06Component::convert_pressure_(const float &p_raw_sc, const float &t_raw_sc) { float p2_raw_sc = p_raw_sc * p_raw_sc; diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 7d9cfecfdf..fe994f440d 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -612,7 +612,7 @@ void SpeakerMediaPlayer::set_volume_(float volume, bool publish) { } // Turn on the mute state if the volume is effectively zero, off otherwise - if (volume < 0.001) { + if (volume < 0.001f) { this->set_mute_state_(true); } else { this->set_mute_state_(false); diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index 87fd4fe9ed..a33a1a1650 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -831,7 +831,7 @@ void SpeakerSourceMediaPlayer::set_volume_(float volume, bool publish) { // Turn on the mute state if the volume is effectively zero, off otherwise. // Pass publish=false to avoid saving twice. - if (volume < 0.001) { + if (volume < 0.001f) { this->set_mute_state_(true, false); } else { this->set_mute_state_(false, false); diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index af42c63bf4..376676ce85 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -215,7 +215,7 @@ void SX126x::configure() { // configure modem if (this->modulation_ == PACKET_TYPE_LORA) { // set modulation params - float duration = 1000.0f * std::pow(2, this->spreading_factor_) / BW_HZ[this->bandwidth_]; + float duration = 1000.0f * (1UL << this->spreading_factor_) / BW_HZ[this->bandwidth_]; buf[0] = this->spreading_factor_; buf[1] = BW_LORA[this->bandwidth_ - SX126X_BW_7810]; buf[2] = this->coding_rate_; diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 0596e91ccc..040a3064bc 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -201,8 +201,8 @@ void SX127x::configure_fsk_ook_() { this->write_register_(REG_OOK_AVG, OOK_AVG_RESERVED | OOK_THRESH_DEC_1_8); // set rx floor - this->write_register_(REG_OOK_FIX, 256 + int(this->rx_floor_ * 2.0)); - this->write_register_(REG_RSSI_THRESH, std::abs(int(this->rx_floor_ * 2.0))); + this->write_register_(REG_OOK_FIX, 256 + int(this->rx_floor_ * 2.0f)); + this->write_register_(REG_RSSI_THRESH, std::abs(int(this->rx_floor_ * 2.0f))); } void SX127x::configure_lora_() { @@ -225,7 +225,7 @@ void SX127x::configure_lora_() { } // optimize detection - float duration = 1000.0f * std::pow(2, this->spreading_factor_) / BW_HZ[this->bandwidth_]; + float duration = 1000.0f * (1UL << this->spreading_factor_) / BW_HZ[this->bandwidth_]; if (duration > 16) { this->write_register_(REG_MODEM_CONFIG3, MODEM_AGC_AUTO_ON | LOW_DATA_RATE_OPTIMIZE_ON); } else { diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 40c65e9f84..b585392790 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -256,7 +256,7 @@ void TCS34725Component::update() { // increase only if not already maximum // do not use max gain, as ist will not get better if (this->gain_reg_ < 3) { - if (((float) raw_c / 655.35 < 20.f) && (this->integration_time_ > 600.f)) { + if (((float) raw_c / 655.35f < 20.f) && (this->integration_time_ > 600.f)) { gain_reg_val_new = this->gain_reg_ + 1; // update integration time to new situation integration_time_ideal = integration_time_ideal / 4; @@ -265,7 +265,7 @@ void TCS34725Component::update() { // decrease gain, if very high clear values and integration times alreadey low if (this->gain_reg_ > 0) { - if (70 < ((float) raw_c / 655.35) && (this->integration_time_ < 200)) { + if (70 < ((float) raw_c / 655.35f) && (this->integration_time_ < 200)) { gain_reg_val_new = this->gain_reg_ - 1; // update integration time to new situation integration_time_ideal = integration_time_ideal * 4; diff --git a/esphome/components/thermopro_ble/thermopro_ble.cpp b/esphome/components/thermopro_ble/thermopro_ble.cpp index 1ccf59a2f6..2a950d3664 100644 --- a/esphome/components/thermopro_ble/thermopro_ble.cpp +++ b/esphome/components/thermopro_ble/thermopro_ble.cpp @@ -196,7 +196,7 @@ static optional parse_tp3(const uint8_t *data, std::size_t data_siz result.humidity = static_cast(data[3]); // battery level, 2 bits (0-2) - result.battery_level = static_cast(data[4] & 0x3) * 50.0; + result.battery_level = static_cast(data[4] & 0x3) * 50.0f; return result; } diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 1b37c6897d..19950bbd15 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -593,7 +593,7 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() { message[3] = ~message[2]; // Byte 4u: Temp if (this->model_ == MODEL_RAC_PT1411HWRU_F) { - temperature = (temperature * 1.8) + 32; + temperature = (temperature * 1.8f) + 32; temp_adjd = temperature - TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN; } diff --git a/esphome/components/ufire_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp index bd2dc2836e..d595b37a83 100644 --- a/esphome/components/ufire_ise/ufire_ise.cpp +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -70,19 +70,19 @@ float UFireISEComponent::measure_ph_(float temperature) { if (mv == -1) return -1; - ph = fabs(7.0 - (mv / PROBE_MV_TO_PH)); + ph = fabsf(7.0f - (mv / PROBE_MV_TO_PH)); // Determine the temperature correction float distance_from_7 = std::abs(7 - roundf(ph)); float distance_from_25 = std::floor(std::abs(25 - roundf(temperature)) / 10); float temp_multiplier = (distance_from_25 * distance_from_7) * PROBE_TMP_CORRECTION; - if ((ph >= 8.0) && (temperature >= 35)) + if ((ph >= 8.0f) && (temperature >= 35)) temp_multiplier *= -1; - if ((ph <= 6.0) && (temperature <= 15)) + if ((ph <= 6.0f) && (temperature <= 15)) temp_multiplier *= -1; ph += temp_multiplier; - if ((ph <= 0.0) || (ph > 14.0)) + if ((ph <= 0.0f) || (ph > 14.0f)) ph = -1; if (std::isinf(ph)) ph = -1; diff --git a/esphome/components/veml3235/veml3235.cpp b/esphome/components/veml3235/veml3235.cpp index fd6cf1e2ed..59892936b0 100644 --- a/esphome/components/veml3235/veml3235.cpp +++ b/esphome/components/veml3235/veml3235.cpp @@ -215,7 +215,7 @@ void VEML3235Sensor::dump_config() { " Auto-gain upper threshold: %f%%\n" " Auto-gain lower threshold: %f%%\n" " Values below will be used as initial values only", - this->auto_gain_threshold_high_ * 100.0, this->auto_gain_threshold_low_ * 100.0); + this->auto_gain_threshold_high_ * 100.0f, this->auto_gain_threshold_low_ * 100.0f); } ESP_LOGCONFIG(TAG, " Digital gain: %uX\n" diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp index 80e6f872ab..594c9da170 100644 --- a/esphome/components/veml7700/veml7700.cpp +++ b/esphome/components/veml7700/veml7700.cpp @@ -380,7 +380,7 @@ void VEML7700Component::apply_lux_compensation_(Readings &data) { // if this light level is exceeded" auto compensate = [&local_data](float &lux) { auto calculate_high_lux_compensation = [](float lux_veml) -> float { - return (((6.0135e-13 * lux_veml - 9.3924e-9) * lux_veml + 8.1488e-5) * lux_veml + 1.0023) * lux_veml; + return (((6.0135e-13f * lux_veml - 9.3924e-9f) * lux_veml + 8.1488e-5f) * lux_veml + 1.0023f) * lux_veml; }; if (lux > 1000.0f || local_data.actual_gain == Gain::X_1_8 || local_data.actual_gain == Gain::X_1_4) { diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index b395c77141..2ade015a25 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -179,12 +179,53 @@ void WiFiComponent::wifi_lazy_init_() { // nor re-register the default WiFi handlers. if (s_sta_netif == nullptr) s_sta_netif = esp_netif_create_default_wifi_sta(); + if (s_sta_netif == nullptr) { + // Allocation failed; leave wifi_initialized_ false so a later enable() retries. + ESP_LOGE(TAG, "esp_netif_create_default_wifi_sta failed"); + return; + } #ifdef USE_WIFI_AP if (s_ap_netif == nullptr) s_ap_netif = esp_netif_create_default_wifi_ap(); #endif // USE_WIFI_AP + // The WiFi driver was started (e.g. by ESP-NOW with the wifi component disabled at + // boot) before our STA netif existed. The default WIFI_EVENT_STA_START handler + // therefore ran with no netif and never called esp_wifi_register_if_rxcb() -- the + // only thing that points the driver's RX path at a netif (it sets + // s_wifi_netifs[WIFI_IF_STA]). A bare esp_netif_action_start() would stop the + // immediate crash (#17232) but leaves RX unbound, so the first association + // associates at L2 yet never receives DHCP replies and times out (#17239). Restart + // the driver now that the netif exists so STA_START re-runs the default handler and + // wires RX correctly. ESP-NOW survives the stop/start (its peer state persists). + // This also matches a self-retry: if esp_wifi_set_storage() below failed on a + // previous wifi_lazy_init_() it returned without setting wifi_initialized_, and + // esp_wifi_init() has since run, so esp_wifi_get_mode() now succeeds here too. + wifi_mode_t mode; + if (esp_wifi_get_mode(&mode) == ESP_OK) { + ESP_LOGD(TAG, "WiFi driver already started without STA netif; restarting to bind it"); + esp_err_t err = esp_wifi_stop(); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_wifi_stop failed: %s", esp_err_to_name(err)); + } + // Re-apply RAM storage; the normal init path does this, but it is skipped on + // the self-retry case above, which would otherwise let the driver persist + // credentials to NVS for the rest of the boot. + err = esp_wifi_set_storage(WIFI_STORAGE_RAM); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_wifi_set_storage failed: %s", esp_err_to_name(err)); + } + err = esp_wifi_start(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_wifi_start failed: %s", esp_err_to_name(err)); + return; + } + s_wifi_started = true; + this->wifi_initialized_ = true; + return; + } + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); if (global_preferences->nvs_handle == 0) { ESP_LOGW(TAG, "starting wifi without nvs"); diff --git a/esphome/components/x9c/x9c.cpp b/esphome/components/x9c/x9c.cpp index 52ce328b3c..b0ad79e51c 100644 --- a/esphome/components/x9c/x9c.cpp +++ b/esphome/components/x9c/x9c.cpp @@ -44,7 +44,7 @@ void X9cOutput::setup() { this->ud_pin_->get_pin(); this->ud_pin_->setup(); - if (this->initial_value_ <= 0.50) { + if (this->initial_value_ <= 0.50f) { this->trim_value(-101); // Set min value (beyond 0) this->trim_value(lroundf(this->initial_value_ * 100)); } else { diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp index a0a9260156..7aa4809e24 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp @@ -49,7 +49,7 @@ bool XiaomiLYWSD03MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device } if (res->humidity.has_value() && this->humidity_ != nullptr) { // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 - *res->humidity = trunc(*res->humidity); + *res->humidity = truncf(*res->humidity); } if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp index 1cf0de14d3..958ac59bde 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp @@ -49,7 +49,7 @@ bool XiaomiMHOC401::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { } if (res->humidity.has_value() && this->humidity_ != nullptr) { // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 - *res->humidity = trunc(*res->humidity); + *res->humidity = truncf(*res->humidity); } if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp index a4303b055a..c2b3ec1437 100644 --- a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp @@ -49,7 +49,7 @@ bool XiaomiXMWSDJ04MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &devic } if (res->humidity.has_value() && this->humidity_ != nullptr) { // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 - *res->humidity = trunc(*res->humidity); + *res->humidity = truncf(*res->humidity); } if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; diff --git a/esphome/config.py b/esphome/config.py index cfe2c52e72..7cde2361cd 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -20,6 +20,7 @@ from esphome.const import ( CONF_ESPHOME, CONF_EXTERNAL_COMPONENTS, CONF_ID, + CONF_MERGE_WARNINGS, CONF_MIN_VERSION, CONF_PACKAGES, CONF_PLATFORM, @@ -1184,6 +1185,24 @@ def validate_config( ) return result + # Warn about any keys silently dropped by `<<` merge includes (shallow, + # first-wins). The esphome: section is now known, so we can honor its + # `merge_warnings:` opt-out. Always drain the queue to keep it from leaking + # into a later run. + if (dropped := yaml_util.take_dropped_merge_keys()) and ( + not isinstance(esphome_conf := config[CONF_ESPHOME], dict) + or esphome_conf.get(CONF_MERGE_WARNINGS, True) + ): + for key, location in dict.fromkeys(dropped): + _LOGGER.warning( + "Key '%s' (%s) was dropped while processing a '<<' merge because it " + "is already defined. Merge keys don't combine sections - the first " + "definition wins. Use 'packages:' to merge sections, or set " + "'esphome: { merge_warnings: false }' to silence this.", + key, + location, + ) + # Snapshot the user's config before any schema validation defaults are # applied. preload_core_config and later validation steps rewrite entries # in-place with defaulted values; deep-copying here preserves the diff --git a/esphome/const.py b/esphome/const.py index 3ca7b2e618..5fa6f00b59 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -613,6 +613,7 @@ CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number" CONF_MEDIA_PLAYER = "media_player" CONF_MEDIUM = "medium" CONF_MEMORY_BLOCKS = "memory_blocks" +CONF_MERGE_WARNINGS = "merge_warnings" CONF_MESSAGE = "message" CONF_METHANE = "methane" CONF_METHOD = "method" diff --git a/esphome/core/config.py b/esphome/core/config.py index 59c96035b8..0670fde0ff 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -26,6 +26,7 @@ from esphome.const import ( CONF_INCLUDES, CONF_INCLUDES_C, CONF_LIBRARIES, + CONF_MERGE_WARNINGS, CONF_MIN_VERSION, CONF_NAME, CONF_NAME_ADD_MAC_SUFFIX, @@ -316,6 +317,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INCLUDES_C, default=[]): cv.ensure_list(valid_include), cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict), cv.Optional(CONF_NAME_ADD_MAC_SUFFIX, default=False): cv.boolean, + cv.Optional(CONF_MERGE_WARNINGS, default=True): cv.boolean, cv.Optional(CONF_DEBUG_SCHEDULER, default=False): cv.boolean, cv.Optional(CONF_PROJECT): cv.Schema( { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 112dde7c45..a7b63643a4 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -669,11 +669,11 @@ void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, if (delta == 0) { hue = 0; } else if (max_color_value == red) { - hue = int(fmod(((60 * ((green - blue) / delta)) + 360), 360)); + hue = int(fmodf((60.0f * ((green - blue) / delta)) + 360.0f, 360.0f)); } else if (max_color_value == green) { - hue = int(fmod(((60 * ((blue - red) / delta)) + 120), 360)); + hue = int(fmodf((60.0f * ((blue - red) / delta)) + 120.0f, 360.0f)); } else if (max_color_value == blue) { - hue = int(fmod(((60 * ((red - green) / delta)) + 240), 360)); + hue = int(fmodf((60.0f * ((red - green) / delta)) + 240.0f, 360.0f)); } if (max_color_value == 0) { @@ -686,8 +686,8 @@ void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, } void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green, float &blue) { float chroma = value * saturation; - float hue_prime = fmod(hue / 60.0, 6); - float intermediate = chroma * (1 - fabs(fmod(hue_prime, 2) - 1)); + float hue_prime = fmodf(hue / 60.0f, 6.0f); + float intermediate = chroma * (1.0f - fabsf(fmodf(hue_prime, 2.0f) - 1.0f)); float delta = value - chroma; if (0 <= hue_prime && hue_prime < 1) { diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 9c5557bdfc..8449cba5e8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -356,7 +356,7 @@ void HOT Scheduler::set_retry_common_(Component *component, NameType name_type, } #endif - if (backoff_increase_factor < 0.0001) { + if (backoff_increase_factor < 0.0001f) { ESP_LOGE(TAG, "set_retry: backoff_factor %0.1f too small, using 1.0: %s", backoff_increase_factor, (name_type == NameType::STATIC_STRING && static_name) ? static_name : ""); backoff_increase_factor = 1; diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index a8e5cf75a8..69cecc58e2 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -239,22 +239,19 @@ def _tar_extract_all( """ Extract a TAR archive to the specified directory. - Implementation is inspired by Python 3.12's tarfile data filtering logic. - This can be replaced with the standard library implementation once - support for Python 3.11 is no longer required. + Path-traversal, link, permission and ownership sanitization is delegated to + the stdlib ``tarfile.data_filter`` (PEP 706). We keep the wrapper-directory + stripping (no stdlib equivalent) and the absolute-path reject (data_filter's + check is os.path-dependent and would miss a Windows drive path when + extracting on POSIX). Args: data: File-like object containing the TAR archive extract_dir: Directory to extract contents to progress_header: If set, show a progress bar with this header """ - import stat import tarfile - # Tar extraction safety: os.path.realpath / commonpath / normpath have no - # pathlib equivalents and Path.resolve() would follow symlinks unsafely. - # Use os.path for the security-sensitive parts; the simple checks move to - # Path. extract_dir = os.fspath(extract_dir) abs_dest = os.path.abspath(extract_dir) # noqa: PTH100 @@ -269,18 +266,14 @@ def _tar_extract_all( safe_members = [] for member in all_members: - name = member.name - - # 1. Strip leading slashes - name = name.lstrip("/" + os.sep) - - # 2. Reject absolute paths (incl. Windows drive) + # Strip leading slashes, then reject absolute / Windows-drive paths + name = member.name.lstrip("/" + os.sep) if Path(name).is_absolute() or ( os.name == "nt" and ":" in name.split(os.sep)[0] # noqa: PTH206 ): continue - # 3. Strip wrapper directory if one was detected + # Strip wrapper directory if one was detected if strip_prefix is not None: norm = name.replace("\\", "/") if norm in (strip_root, strip_prefix): @@ -288,88 +281,29 @@ def _tar_extract_all( if not norm.startswith(strip_prefix): continue name = norm[len(strip_prefix) :] - - # 4. Compute final path - target_path = os.path.realpath(os.path.join(abs_dest, name)) # noqa: PTH118 - if os.path.commonpath([abs_dest, target_path]) != abs_dest: - continue - - # 5. Validate links properly - if member.issym() or member.islnk(): - linkname = member.linkname - - # Reject absolute link targets - if Path(linkname).is_absolute(): - continue - - if member.islnk() and strip_prefix is not None: - # Hard-link linknames reference another archive member - # by its archive name. We've stripped the wrapper prefix - # from member.name above (step 3); strip it here too so - # tarfile._find_link_target can resolve the target during - # extraction. Symlink linknames are filesystem-relative - # paths, not archive-member references, so they don't - # need this treatment. - norm_link = linkname.replace("\\", "/") - if norm_link in (strip_root, strip_prefix): - continue - if not norm_link.startswith(strip_prefix): - continue - linkname = norm_link[len(strip_prefix) :] - - # Strip leading slashes - linkname = os.path.normpath(linkname) - - if member.issym(): - link_target = os.path.join( # noqa: PTH118 - abs_dest, - os.path.dirname(name), # noqa: PTH120 - linkname, - ) - else: - link_target = os.path.join(abs_dest, linkname) # noqa: PTH118 - link_target = os.path.realpath(link_target) - - if os.path.commonpath([abs_dest, link_target]) != abs_dest: - continue - - # write back normalized linkname - member.linkname = linkname - - # 6. Sanitize permissions - mode = member.mode - if mode is not None: - # Strip high bits & group/other write bits - mode &= ( - stat.S_IRWXU - | stat.S_IRGRP - | stat.S_IXGRP - | stat.S_IROTH - | stat.S_IXOTH - ) - if member.isfile() or member.islnk(): - # remove exec bits unless explicitly user-executable - if not (mode & stat.S_IXUSR): - mode &= ~(stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - mode |= stat.S_IRUSR | stat.S_IWUSR - elif not (member.isdir() or member.issym()): - # Block special files. Directories and symlinks keep - # their masked-original mode — passing None here would - # crash tarfile.extract on Python <3.12 (its chmod - # path calls os.chmod unconditionally). - continue - - member.mode = mode - - # 7. Strip ownership - member.uid = None - member.gid = None - member.uname = None - member.gname = None - - # 8. Assign sanitized name back member.name = name + # Hard-link linknames reference another archive member by its + # archive name; strip the wrapper prefix here too so + # tarfile._find_link_target can resolve the target during + # extraction. Symlink linknames are filesystem-relative paths, + # not archive-member references, so they don't need this. + if member.islnk() and strip_prefix is not None: + norm_link = member.linkname.replace("\\", "/") + if norm_link in (strip_root, strip_prefix): + continue + if not norm_link.startswith(strip_prefix): + continue + member.linkname = norm_link[len(strip_prefix) :] + + # Delegate traversal, link, permission and ownership sanitization + # to the stdlib data filter; it raises FilterError for unsafe + # members (path traversal, links outside dest, special files). + try: + member = tarfile.data_filter(member, abs_dest) + except tarfile.FilterError: + continue + safe_members.append(member) total = len(safe_members) diff --git a/esphome/helpers.py b/esphome/helpers.py index 62dfd0fb09..631bcb6f39 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -397,17 +397,13 @@ def rmtree(path: Path | str) -> None: read-only flag and retrying. """ - def _onerror(func, path, exc_info): + def _onexc(func, path, exc): if os.access(path, os.W_OK): - raise exc_info[1].with_traceback(exc_info[2]) + raise exc Path(path).chmod(stat.S_IWUSR | stat.S_IRUSR) func(path) - # ``onerror`` is deprecated in 3.12 in favour of ``onexc`` (different - # callable signature); keep the existing handler shape for now and - # silence the lint locally so this PR doesn't bundle an unrelated - # migration. - shutil.rmtree(path, onerror=_onerror) # pylint: disable=deprecated-argument + shutil.rmtree(path, onexc=_onexc) def walk_files(path: Path): diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 81c16f2e38..4f36e4dbe6 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -12,7 +12,7 @@ dependencies: esphome/micro-flac: version: 0.2.0 esphome/micro-mp3: - version: 0.3.0 + version: 0.4.0 esphome/micro-opus: version: 0.4.1 esphome/micro-wav: diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 43282c7aa0..c2d783ecbe 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -24,7 +24,7 @@ import os from pathlib import Path import re import tempfile -from typing import Any, TypeVar +from typing import Any from urllib.parse import urlparse, urlsplit, urlunsplit from esphome import git @@ -195,10 +195,7 @@ class LibraryBackend: emit: Callable[["ConvertedLibrary"], None] -T = TypeVar("T") - - -def ensure_list(obj: T | list[T]) -> list[T]: +def ensure_list[T](obj: T | list[T]) -> list[T]: """ Convert an object to a list if it isn't already a list. diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index bfe1fb0136..0009cde551 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -51,6 +51,29 @@ _load_listeners: list[Callable[[Path], None]] = [] DocumentPath = list[str | int] +# Key under CORE.data used to accumulate keys that a `<<` merge silently +# dropped. The warning is emitted later (see esphome.config.validate_config), +# because the esphome: option that suppresses it isn't known while parsing. +_MERGE_WARNINGS_KEY = "yaml_dropped_merge_keys" + + +def _record_dropped_merge_key(parent_file: Path, key: Any) -> None: + """Record a mapping key that a ``<<`` merge silently dropped. + + Merge keys follow the YAML spec's shallow, first-wins semantics: a key that + already exists in the mapping (or came from an earlier merge) is discarded + rather than deep-merged the way ``packages:`` would combine it. We collect + these so a single warning can be emitted once the config is loaded. + """ + esp_range = getattr(key, "esp_range", None) + location = str(esp_range.start_mark) if esp_range is not None else str(parent_file) + CORE.data.setdefault(_MERGE_WARNINGS_KEY, []).append((str(key), location)) + + +def take_dropped_merge_keys() -> list[tuple[str, str]]: + """Return and clear the keys dropped during ``<<`` merges so far.""" + return CORE.data.pop(_MERGE_WARNINGS_KEY, []) + class SensitiveStr(str): """Marker subclass for validated strings that should be masked in @@ -551,6 +574,10 @@ class ESPHomeLoaderMixin: # is expected to contain mapping nodes and each of these nodes is merged in # turn according to its order in the sequence. Keys in mapping nodes earlier # in the sequence override keys specified in later mapping nodes." + # + # This is a silent shallow drop (unlike `packages:`, which deep-merges). + # Record it so a warning can be emitted after the config loads. + _record_dropped_merge_key(self.name, key) continue pairs.append((key, value)) # Add key node to seen keys, for sequence merge values. diff --git a/script/ci-custom.py b/script/ci-custom.py index 6c5ad5bb69..4568732b88 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -555,7 +555,7 @@ def lint_constants_usage(): # Maximum allowed CONF_ constants in esphome/const.py. # This file is frozen — new constants go in esphome/components/const/__init__.py. # Decrease this number when constants are moved out of const.py. -CONST_PY_MAX_CONF = 1013 +CONST_PY_MAX_CONF = 1014 @lint_content_check(include=["esphome/const.py"]) diff --git a/script/lint-python b/script/lint-python index e4b3314d2a..6bd95778fa 100755 --- a/script/lint-python +++ b/script/lint-python @@ -139,7 +139,7 @@ def main(): print() print("Running pyupgrade...") print() - PYUPGRADE_TARGET = "--py311-plus" + PYUPGRADE_TARGET = "--py312-plus" for files in filesets: cmd = ["pyupgrade", PYUPGRADE_TARGET] + files log = get_err(*cmd) diff --git a/tests/components/modbus/modbus_helpers_test.cpp b/tests/components/modbus/modbus_helpers_test.cpp index cd260f410a..ecdca4df6d 100644 --- a/tests/components/modbus/modbus_helpers_test.cpp +++ b/tests/components/modbus/modbus_helpers_test.cpp @@ -194,4 +194,40 @@ TEST(ModbusHelpersTest, PayloadToNumberDecodesValidWord) { EXPECT_EQ(payload_to_number(data, SensorValueType::U_WORD, 0, 0xFFFFFFFF), 0x1234); } +// --- registers_to_number --------------------------------------------------- +// Register words are host byte order; results must match the byte-based payload_to_number. + +TEST(ModbusHelpersTest, RegistersToNumberDecodesWord) { + const uint16_t registers[] = {0x1234}; + EXPECT_EQ(registers_to_number(registers, 1, SensorValueType::U_WORD), 0x1234); +} + +TEST(ModbusHelpersTest, RegistersToNumberDecodesDwordHighWordFirst) { + const uint16_t registers[] = {0x1234, 0x5678}; + EXPECT_EQ(registers_to_number(registers, 2, SensorValueType::U_DWORD), 0x12345678); +} + +TEST(ModbusHelpersTest, RegistersToNumberDecodesAtSpanStart) { + // The function decodes the value at the start of the span; the caller advances the pointer. + const uint16_t registers[] = {0xAAAA, 0x1234}; + EXPECT_EQ(registers_to_number(registers + 1, 1, SensorValueType::U_WORD), 0x1234); +} + +TEST(ModbusHelpersTest, RegistersToNumberMatchesPayloadToNumber) { + // Same value via both decoders: registers (host order) vs big-endian bytes. + const uint16_t registers[] = {0x8001, 0x0002}; + const std::vector bytes{0x80, 0x01, 0x00, 0x02}; + for (auto value_type : {SensorValueType::S_DWORD, SensorValueType::U_DWORD, SensorValueType::S_DWORD_R}) { + EXPECT_EQ(registers_to_number(registers, 2, value_type), payload_to_number(bytes, value_type, 0, 0xFFFFFFFF)) + << "value_type=" << static_cast(value_type); + } +} + +TEST(ModbusHelpersTest, RegistersToNumberRejectsTruncatedMultiRegisterValue) { + const uint16_t registers[] = {0x1234}; + bool error = false; + EXPECT_EQ(registers_to_number(registers, 1, SensorValueType::U_DWORD, &error), 0); + EXPECT_TRUE(error); +} + } // namespace esphome::modbus::helpers diff --git a/tests/components/modbus_server/modbus_server_test.cpp b/tests/components/modbus_server/modbus_server_test.cpp new file mode 100644 index 0000000000..0c8f5d04cf --- /dev/null +++ b/tests/components/modbus_server/modbus_server_test.cpp @@ -0,0 +1,124 @@ +#include + +#include "esphome/components/modbus_server/modbus_server.h" + +namespace esphome::modbus_server { + +using modbus::ModbusExceptionCode; +using modbus::RegisterValues; + +namespace { + +RegisterValues make_registers(std::initializer_list values) { + RegisterValues registers; + for (uint16_t value : values) + registers.push_back(value); + return registers; +} + +} // namespace + +// A single writable WORD register is applied and the handler reports success (nullopt). +TEST(ModbusServerWrite, SingleWordSucceeds) { + ModbusServer server; + int64_t written = -1; + ServerRegister reg(0x0000, SensorValueType::U_WORD, 1); + reg.write_lambda = [&written](int64_t value) { + written = value; + return true; + }; + server.add_server_register(®); + + auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234})); + EXPECT_FALSE(status.has_value()); // nullopt == success + EXPECT_EQ(written, 0x1234); +} + +// A multi-register value is decoded high word first and applied as a single number. +TEST(ModbusServerWrite, DwordSucceeds) { + ModbusServer server; + int64_t written = -1; + ServerRegister reg(0x0000, SensorValueType::U_DWORD, 2); + reg.write_lambda = [&written](int64_t value) { + written = value; + return true; + }; + server.add_server_register(®); + + auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234, 0x5678})); + EXPECT_FALSE(status.has_value()); + EXPECT_EQ(written, 0x12345678); +} + +// Regression: a request that under-supplies a multi-register value is rejected before any +// write_lambda runs, so no register is partially written. +TEST(ModbusServerWrite, UnderSuppliedValueAppliesNothing) { + ModbusServer server; + bool word_written = false; + ServerRegister word_reg(0x0000, SensorValueType::U_WORD, 1); + word_reg.write_lambda = [&word_written](int64_t) { + word_written = true; + return true; + }; + bool dword_written = false; + ServerRegister dword_reg(0x0001, SensorValueType::U_DWORD, 2); // needs two registers + dword_reg.write_lambda = [&dword_written](int64_t) { + dword_written = true; + return true; + }; + server.add_server_register(&word_reg); + server.add_server_register(&dword_reg); + + // Two words supplied: one for the WORD at 0x0000, but only one of the two the DWORD at 0x0001 needs. + auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1111, 0x2222})); + ASSERT_TRUE(status.has_value()); + if (status.has_value()) + EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_VALUE); + EXPECT_FALSE(word_written); // the writable WORD must NOT have been applied + EXPECT_FALSE(dword_written); +} + +// A read-only register (no write_lambda) yields ILLEGAL_DATA_ADDRESS and applies nothing. +TEST(ModbusServerWrite, UnwritableRegisterRejected) { + ModbusServer server; + ServerRegister read_only(0x0000, SensorValueType::U_WORD, 1); // no write_lambda set + server.add_server_register(&read_only); + + auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234})); + ASSERT_TRUE(status.has_value()); + if (status.has_value()) + EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); +} + +// An address with no registered register yields ILLEGAL_DATA_ADDRESS. +TEST(ModbusServerWrite, UnmatchedAddressRejected) { + ModbusServer server; + auto status = server.on_modbus_write_registers(0x0005, make_registers({0x1234})); + ASSERT_TRUE(status.has_value()); + if (status.has_value()) + EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS); +} + +// A write_lambda failing at runtime is the one non-atomic case: the earlier register is already +// applied, and the handler reports SERVICE_DEVICE_FAILURE. +TEST(ModbusServerWrite, CallbackFailureIsServiceDeviceFailure) { + ModbusServer server; + bool first_written = false; + ServerRegister first(0x0000, SensorValueType::U_WORD, 1); + first.write_lambda = [&first_written](int64_t) { + first_written = true; + return true; + }; + ServerRegister second(0x0001, SensorValueType::U_WORD, 1); + second.write_lambda = [](int64_t) { return false; }; // rejects at runtime + server.add_server_register(&first); + server.add_server_register(&second); + + auto status = server.on_modbus_write_registers(0x0000, make_registers({0xAAAA, 0xBBBB})); + ASSERT_TRUE(status.has_value()); + if (status.has_value()) + EXPECT_EQ(status.value(), ModbusExceptionCode::SERVICE_DEVICE_FAILURE); + EXPECT_TRUE(first_written); // pre-validation passed, so the first write applied before the failure +} + +} // namespace esphome::modbus_server diff --git a/tests/integration/fixtures/uart_mock_modbus_broadcast.yaml b/tests/integration/fixtures/uart_mock_modbus_broadcast.yaml deleted file mode 100644 index a5ce02b342..0000000000 --- a/tests/integration/fixtures/uart_mock_modbus_broadcast.yaml +++ /dev/null @@ -1,56 +0,0 @@ -esphome: - name: uart-mock-modbus-bcast - -host: -api: -logger: - level: VERBOSE - -external_components: - - source: - type: local - path: EXTERNAL_COMPONENT_PATH - -# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] -# The actual UART bus used is the uart_mock component below -uart: - baud_rate: 115200 - port: /dev/null - -# No on_tx injection: a broadcast (address 0) gets no reply on a real bus. -uart_mock: - - id: virtual_uart - baud_rate: 9600 - auto_start: true - debug: - -modbus: - - uart_id: virtual_uart - id: virtual_modbus - role: client - send_wait_time: 200ms - turnaround_time: 10ms - -modbus_controller: - - address: 0 - modbus_id: virtual_modbus - update_interval: 60s - id: modbus_controller_bcast - -number: - - platform: modbus_controller - modbus_controller_id: modbus_controller_bcast - id: bcast_write - name: "bcast_write" - address: 0x01 - register_type: holding - value_type: U_WORD - min_value: 0 - max_value: 65535 - -interval: - - interval: 400ms - then: - - number.set: - id: bcast_write - value: 42 diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index c8517aff09..65af57b944 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -19,7 +19,6 @@ from aioesphomeapi import ( _LOGGER = logging.getLogger(__name__) -T = TypeVar("T", bound=EntityInfo) S = TypeVar("S", bound=EntityState) @@ -58,7 +57,7 @@ async def wait_for_state( return await asyncio.wait_for(future, timeout=timeout) -def find_entity( +def find_entity[T: EntityInfo]( entities: list[EntityInfo], object_id_substring: str, entity_type: type[T] | None = None, @@ -86,7 +85,7 @@ def find_entity( return None -def require_entity( +def require_entity[T: EntityInfo]( entities: list[EntityInfo], object_id_substring: str, entity_type: type[T] | None = None, diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index 385707d849..2c437341c6 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -330,28 +330,3 @@ async def test_uart_mock_modbus_server_controller_multiple( await tracker.setup_and_start_scenario(client) await tracker.await_all(futures) _assert_no_modbus_errors(error_log_lines, warning_log_lines) - - -@pytest.mark.asyncio -async def test_uart_mock_modbus_broadcast( - yaml_config: str, - run_compiled: RunCompiledFunction, - api_client_connected: APIClientConnectedFactory, -) -> None: - """Test that broadcast writes (address 0) don't wait for a response. - - A controller at address 0 sends broadcast writes that get no reply. The - client must not arm the response timeout for them: otherwise every write - blocks for send_wait_time and logs a spurious "no response from 0" warning. - """ - - line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() - - async with ( - run_compiled(yaml_config, line_callback=line_callback), - api_client_connected(), - ): - # Several broadcast writes fire on the 400ms interval; send_wait_time is - # 200ms, so the old behaviour would have warned on each one by now. - await asyncio.sleep(3.0) - _assert_no_modbus_errors(error_log_lines, warning_log_lines) diff --git a/tests/unit_tests/test_config_normalization.py b/tests/unit_tests/test_config_normalization.py index 4ec17b3c7c..a06b2da621 100644 --- a/tests/unit_tests/test_config_normalization.py +++ b/tests/unit_tests/test_config_normalization.py @@ -1,6 +1,7 @@ """Unit tests for esphome.config module.""" from collections.abc import Generator +import logging from pathlib import Path from unittest.mock import MagicMock, Mock, patch @@ -113,3 +114,57 @@ def test_ota_with_platform_list_and_captive_portal(fixtures_dir: Path) -> None: platforms = {p.get("platform") for p in result["ota"]} assert "esphome" in platforms, f"Expected esphome platform in {platforms}" assert "web_server" in platforms, f"Expected web_server platform in {platforms}" + + +def _write_merge_conflict_config(tmp_path: Path, *, suppress: bool) -> Path: + """Create a config where two `<<` includes both define `logger:`. + + The second `logger:` is dropped by the shallow merge. Returns the main file. + """ + (tmp_path / "a.yaml").write_text("logger:\n level: DEBUG\n") + (tmp_path / "b.yaml").write_text("logger:\n level: INFO\n") + esphome_section = "esphome:\n name: test\n" + if suppress: + esphome_section += " merge_warnings: false\n" + main = tmp_path / "main.yaml" + main.write_text(f"{esphome_section}<<: !include a.yaml\n<<: !include b.yaml\n") + return main + + +def test_validate_config_warns_on_dropped_merge_key( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """By default, a `<<` merge that drops a key logs a warning.""" + main = _write_merge_conflict_config(tmp_path, suppress=False) + CORE.config_path = main + raw_config = yaml_util.load_yaml(main) + + with caplog.at_level(logging.WARNING, logger="esphome.config"): + config.validate_config(raw_config, {}) + + assert any( + "was dropped while processing a '<<' merge" in record.message + and "logger" in record.message + for record in caplog.records + ) + # The queue is drained so the warning cannot leak into a later run. + assert yaml_util.take_dropped_merge_keys() == [] + + +def test_validate_config_suppresses_merge_warning( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """`esphome: merge_warnings: false` hides the warning but still drains the queue.""" + main = _write_merge_conflict_config(tmp_path, suppress=True) + CORE.config_path = main + raw_config = yaml_util.load_yaml(main) + + with caplog.at_level(logging.WARNING, logger="esphome.config"): + config.validate_config(raw_config, {}) + + assert not any( + "was dropped while processing a '<<' merge" in record.message + for record in caplog.records + ) + # The queue is drained even when the warning is suppressed. + assert yaml_util.take_dropped_merge_keys() == [] diff --git a/tests/unit_tests/test_framework_helpers.py b/tests/unit_tests/test_framework_helpers.py index fd807ed05d..6fe62dcc8c 100644 --- a/tests/unit_tests/test_framework_helpers.py +++ b/tests/unit_tests/test_framework_helpers.py @@ -658,11 +658,6 @@ def test_get_python_env_executable_path_nt() -> None: class TestTarExtractAllBranches: - @pytest.mark.skipif( - sys.version_info < (3, 12), - reason="patching os.name makes pathlib build a WindowsPath, which only " - "instantiates on POSIX in 3.12+", - ) def test_windows_drive_path_skipped(self, tmp_path: Path) -> None: """Windows-style drive path (C:/...) is skipped when os.name == 'nt'.""" info = tarfile.TarInfo(name="C:/secret.txt") @@ -755,11 +750,6 @@ class TestTarExtractAllBranches: class TestZipExtractAllBranches: - @pytest.mark.skipif( - sys.version_info < (3, 12), - reason="patching os.name makes pathlib build a WindowsPath, which only " - "instantiates on POSIX in 3.12+", - ) def test_windows_drive_path_skipped(self, tmp_path: Path) -> None: """Windows-style drive path (C:/...) is skipped when os.name == 'nt'.""" buf = _make_zip([("C:/secret.txt", "bad")]) diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 6be090b869..fa1c0fcce2 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -1395,3 +1395,48 @@ def test_dump__redaction_flag_does_not_leak_between_calls() -> None: assert "\\033[8m" in redacted assert "\\033[8m" not in raw assert "\\033[8m" in redacted_again + + +@pytest.fixture(autouse=True) +def clear_dropped_merge_keys() -> None: + """Reset the dropped-merge-key queue between tests.""" + core.CORE.data.pop(yaml_util._MERGE_WARNINGS_KEY, None) + yield + core.CORE.data.pop(yaml_util._MERGE_WARNINGS_KEY, None) + + +def test_merge_include_records_dropped_keys(tmp_path: Path) -> None: + """A `<<` merge that overlaps an existing key records it (shallow first-wins).""" + (tmp_path / "a.yaml").write_text("api:\n reboot_timeout: 5min\n") + (tmp_path / "b.yaml").write_text("api:\n password: secret\n") + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text("<<: !include a.yaml\n<<: !include b.yaml\n") + + with patch.object(core.CORE, "config_path", test_yaml): + result = yaml_util.load_yaml(test_yaml) + + # First definition wins; the second `api` block is dropped entirely. + assert result["api"] == {"reboot_timeout": "5min"} + + dropped = yaml_util.take_dropped_merge_keys() + assert len(dropped) == 1 + key, location = dropped[0] + assert key == "api" + assert "b.yaml" in location + # Queue is drained after being taken. + assert yaml_util.take_dropped_merge_keys() == [] + + +def test_merge_include_no_overlap_records_nothing(tmp_path: Path) -> None: + """A `<<` merge with distinct top-level keys drops nothing.""" + (tmp_path / "a.yaml").write_text("api:\n reboot_timeout: 5min\n") + (tmp_path / "b.yaml").write_text("logger:\n level: DEBUG\n") + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text("<<: !include a.yaml\n<<: !include b.yaml\n") + + with patch.object(core.CORE, "config_path", test_yaml): + result = yaml_util.load_yaml(test_yaml) + + assert result["api"] == {"reboot_timeout": "5min"} + assert result["logger"] == {"level": "DEBUG"} + assert yaml_util.take_dropped_merge_keys() == []