diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index e95eb6331f..c53e64a7b9 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -22,7 +22,7 @@ runs: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.0 + uses: actions/cache/restore@v4.2.1 with: path: venv # yamllint disable-line rule:line-length diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab77db5ca5..59dc31e9f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.2.0 + uses: actions/cache@v4.2.1 with: path: venv # yamllint disable-line rule:line-length @@ -61,8 +61,8 @@ jobs: pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -e . - black: - name: Check black + ruff: + name: Check ruff runs-on: ubuntu-24.04 needs: - common @@ -74,10 +74,10 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - - name: Run black + - name: Run Ruff run: | . venv/bin/activate - black --verbose esphome tests + ruff format esphome tests - name: Suggested changes run: script/ci-suggest-changes if: always() @@ -255,7 +255,7 @@ jobs: runs-on: ubuntu-24.04 needs: - common - - black + - ruff - ci-custom - clang-format - flake8 @@ -303,14 +303,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@v4.2.0 + uses: actions/cache@v4.2.1 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@v4.2.0 + uses: actions/cache/restore@v4.2.1 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }} @@ -482,7 +482,7 @@ jobs: runs-on: ubuntu-24.04 needs: - common - - black + - ruff - ci-custom - clang-format - flake8 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 212d822ff8..667a8f2e8b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.5.4 + rev: v0.9.2 hooks: # Run the linter. - id: ruff diff --git a/CODEOWNERS b/CODEOWNERS index 5025977f10..3775a58686 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -234,6 +234,7 @@ esphome/components/kuntze/* @ssieb esphome/components/lcd_menu/* @numo68 esphome/components/ld2410/* @regevbr @sebcaps esphome/components/ld2420/* @descipher +esphome/components/ld2450/* @hareeshmu esphome/components/ledc/* @OttoWinter esphome/components/libretiny/* @kuba2k2 esphome/components/libretiny_pwm/* @kuba2k2 diff --git a/docker/Dockerfile b/docker/Dockerfile index 1db1ee7b51..6da5c52d64 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -35,7 +35,7 @@ RUN \ iputils-ping=3:20221126-1+deb12u1 \ git=1:2.39.5-0+deb12u1 \ curl=7.88.1-10+deb12u8 \ - openssh-client=1:9.2p1-2+deb12u3 \ + openssh-client=1:9.2p1-2+deb12u4 \ python3-cffi=1.15.1-5 \ libcairo2=1.16.0-7 \ libmagic1=1:5.44-3 \ diff --git a/esphome/__main__.py b/esphome/__main__.py index 2a0bd8f2b3..770c1a8fcf 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -66,7 +66,7 @@ def choose_prompt(options, purpose: str = None): return options[0][1] safe_print( - f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:' + f"Found multiple options{f' for {purpose}' if purpose else ''}, please choose one:" ) for i, (desc, _) in enumerate(options): safe_print(f" [{i + 1}] {desc}") diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index aa705e7332..445507c620 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -128,7 +128,6 @@ VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema( def visual_temperature_step(value): - # Allow defining target/current temperature steps separately if isinstance(value, dict): return VISUAL_TEMPERATURE_STEP_SCHEMA(value) diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index f97f289a0a..6e0d103aa0 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -66,7 +66,9 @@ FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant async def to_code(config): uuid = config[CONF_UUID].hex - uuid_arr = [cg.RawExpression(f"0x{uuid[i:i + 2]}") for i in range(0, len(uuid), 2)] + uuid_arr = [ + cg.RawExpression(f"0x{uuid[i : i + 2]}") for i in range(0, len(uuid), 2) + ] var = cg.new_Pvariable(config[CONF_ID], uuid_arr) parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 53016d2130..050efaacae 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -112,8 +112,7 @@ def validate_supports(value): ) if is_pullup and num == 16: raise cv.Invalid( - "GPIO Pin 16 does not support pullup pin mode. " - "Please choose another pin.", + "GPIO Pin 16 does not support pullup pin mode. Please choose another pin.", [CONF_MODE, CONF_PULLUP], ) if is_pulldown and num != 16: diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index f2dc7174cb..f77d624649 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -1,9 +1,15 @@ -import logging -import esphome.codegen as cg -import esphome.config_validation as cv -import esphome.final_validate as fv -from esphome.components import uart, climate, logger +import logging + from esphome import automation +import esphome.codegen as cg +from esphome.components import climate, logger, uart +from esphome.components.climate import ( + CONF_CURRENT_TEMPERATURE, + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) +import esphome.config_validation as cv from esphome.const import ( CONF_BEEPER, CONF_DISPLAY, @@ -24,12 +30,7 @@ from esphome.const import ( CONF_VISUAL, CONF_WIFI, ) -from esphome.components.climate import ( - ClimateMode, - ClimatePreset, - ClimateSwingMode, - CONF_CURRENT_TEMPERATURE, -) +import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py new file mode 100644 index 0000000000..37f68a8f3e --- /dev/null +++ b/esphome/components/ld2450/__init__.py @@ -0,0 +1,51 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import ( + CONF_ID, + CONF_THROTTLE, +) + +DEPENDENCIES = ["uart"] +CODEOWNERS = ["@hareeshmu"] +MULTI_CONF = True + +ld2450_ns = cg.esphome_ns.namespace("ld2450") +LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice) + +CONF_LD2450_ID = "ld2450_id" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LD2450Component), + cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(min=cv.TimePeriod(milliseconds=1)), + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +LD2450BaseSchema = cv.Schema( + { + cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), + }, +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "ld2450", + require_tx=True, + require_rx=True, + parity="NONE", + stop_bits=1, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + cg.add(var.set_throttle(config[CONF_THROTTLE])) diff --git a/esphome/components/ld2450/binary_sensor.py b/esphome/components/ld2450/binary_sensor.py new file mode 100644 index 0000000000..d0082ac21a --- /dev/null +++ b/esphome/components/ld2450/binary_sensor.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HAS_MOVING_TARGET, + CONF_HAS_STILL_TARGET, + CONF_HAS_TARGET, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, +) + +from . import CONF_LD2450_ID, LD2450Component + +DEPENDENCIES = ["ld2450"] + +ICON_MEDITATION = "mdi:meditation" +ICON_SHIELD_ACCOUNT = "mdi:shield-account" +ICON_TARGET_ACCOUNT = "mdi:target-account" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), + cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, + icon=ICON_SHIELD_ACCOUNT, + ), + cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_MOTION, + icon=ICON_TARGET_ACCOUNT, + ), + cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, + icon=ICON_MEDITATION, + ), +} + + +async def to_code(config): + ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) + if has_target_config := config.get(CONF_HAS_TARGET): + sens = await binary_sensor.new_binary_sensor(has_target_config) + cg.add(ld2450_component.set_target_binary_sensor(sens)) + if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET): + sens = await binary_sensor.new_binary_sensor(has_moving_target_config) + cg.add(ld2450_component.set_moving_target_binary_sensor(sens)) + if has_still_target_config := config.get(CONF_HAS_STILL_TARGET): + sens = await binary_sensor.new_binary_sensor(has_still_target_config) + cg.add(ld2450_component.set_still_target_binary_sensor(sens)) diff --git a/esphome/components/ld2450/button/__init__.py b/esphome/components/ld2450/button/__init__.py new file mode 100644 index 0000000000..39671d3a3b --- /dev/null +++ b/esphome/components/ld2450/button/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ( + CONF_FACTORY_RESET, + CONF_RESTART, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_RESTART, + ICON_RESTART_ALERT, +) + +from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns + +ResetButton = ld2450_ns.class_("ResetButton", button.Button) +RestartButton = ld2450_ns.class_("RestartButton", button.Button) + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), + cv.Optional(CONF_FACTORY_RESET): button.button_schema( + ResetButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ), + cv.Optional(CONF_RESTART): button.button_schema( + RestartButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_RESTART, + ), +} + + +async def to_code(config): + ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) + if factory_reset_config := config.get(CONF_FACTORY_RESET): + b = await button.new_button(factory_reset_config) + await cg.register_parented(b, config[CONF_LD2450_ID]) + cg.add(ld2450_component.set_reset_button(b)) + if restart_config := config.get(CONF_RESTART): + b = await button.new_button(restart_config) + await cg.register_parented(b, config[CONF_LD2450_ID]) + cg.add(ld2450_component.set_restart_button(b)) diff --git a/esphome/components/ld2450/button/reset_button.cpp b/esphome/components/ld2450/button/reset_button.cpp new file mode 100644 index 0000000000..e96ec99cc5 --- /dev/null +++ b/esphome/components/ld2450/button/reset_button.cpp @@ -0,0 +1,9 @@ +#include "reset_button.h" + +namespace esphome { +namespace ld2450 { + +void ResetButton::press_action() { this->parent_->factory_reset(); } + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/button/reset_button.h b/esphome/components/ld2450/button/reset_button.h new file mode 100644 index 0000000000..73804fa6d6 --- /dev/null +++ b/esphome/components/ld2450/button/reset_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2450.h" + +namespace esphome { +namespace ld2450 { + +class ResetButton : public button::Button, public Parented { + public: + ResetButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/button/restart_button.cpp b/esphome/components/ld2450/button/restart_button.cpp new file mode 100644 index 0000000000..ee2f5ac12f --- /dev/null +++ b/esphome/components/ld2450/button/restart_button.cpp @@ -0,0 +1,9 @@ +#include "restart_button.h" + +namespace esphome { +namespace ld2450 { + +void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/button/restart_button.h b/esphome/components/ld2450/button/restart_button.h new file mode 100644 index 0000000000..a44ae5a4d2 --- /dev/null +++ b/esphome/components/ld2450/button/restart_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2450.h" + +namespace esphome { +namespace ld2450 { + +class RestartButton : public button::Button, public Parented { + public: + RestartButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp new file mode 100644 index 0000000000..044ee5ef8b --- /dev/null +++ b/esphome/components/ld2450/ld2450.cpp @@ -0,0 +1,867 @@ +#include "ld2450.h" +#include +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#include "esphome/core/component.h" + +#define highbyte(val) (uint8_t)((val) >> 8) +#define lowbyte(val) (uint8_t)((val) &0xff) + +namespace esphome { +namespace ld2450 { + +static const char *const TAG = "ld2450"; +static const char *const UNKNOWN_MAC("unknown"); + +// LD2450 UART Serial Commands +static const uint8_t CMD_ENABLE_CONF = 0x00FF; +static const uint8_t CMD_DISABLE_CONF = 0x00FE; +static const uint8_t CMD_VERSION = 0x00A0; +static const uint8_t CMD_MAC = 0x00A5; +static const uint8_t CMD_RESET = 0x00A2; +static const uint8_t CMD_RESTART = 0x00A3; +static const uint8_t CMD_BLUETOOTH = 0x00A4; +static const uint8_t CMD_SINGLE_TARGET_MODE = 0x0080; +static const uint8_t CMD_MULTI_TARGET_MODE = 0x0090; +static const uint8_t CMD_QUERY_TARGET_MODE = 0x0091; +static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; +static const uint8_t CMD_QUERY_ZONE = 0x00C1; +static const uint8_t CMD_SET_ZONE = 0x00C2; + +static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; + +static inline std::string convert_signed_int_to_hex(int value) { + auto value_as_str = str_snprintf("%04x", 4, value & 0xFFFF); + return value_as_str; +} + +static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) { + for (int i = 0; i < 4; i++) { + std::string temp_hex = convert_signed_int_to_hex(values[i]); + bytes[i * 2] = std::stoi(temp_hex.substr(2, 2), nullptr, 16); // Store high byte + bytes[i * 2 + 1] = std::stoi(temp_hex.substr(0, 2), nullptr, 16); // Store low byte + } +} + +static inline int16_t decode_coordinate(uint8_t low_byte, uint8_t high_byte) { + int16_t coordinate = (high_byte & 0x7F) << 8 | low_byte; + if ((high_byte & 0x80) == 0) { + coordinate = -coordinate; + } + return coordinate; // mm +} + +static inline int16_t decode_speed(uint8_t low_byte, uint8_t high_byte) { + int16_t speed = (high_byte & 0x7F) << 8 | low_byte; + if ((high_byte & 0x80) == 0) { + speed = -speed; + } + return speed * 10; // mm/s +} + +static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) { + uint16_t hex_val = (buffer[offset + 1] << 8) | buffer[offset]; + int16_t dec_val = static_cast(hex_val); + if (dec_val & 0x8000) { + dec_val -= 65536; + } + return dec_val; +} + +static inline float calculate_angle(float base, float hypotenuse) { + if (base < 0.0 || hypotenuse <= 0.0) { + return 0.0; + } + float angle_radians = std::acos(base / hypotenuse); + float angle_degrees = angle_radians * (180.0 / M_PI); + return angle_degrees; +} + +static inline std::string get_direction(int16_t speed) { + static const char *const APPROACHING = "Approaching"; + static const char *const MOVING_AWAY = "Moving away"; + static const char *const STATIONARY = "Stationary"; + + if (speed > 0) { + return MOVING_AWAY; + } + if (speed < 0) { + return APPROACHING; + } + return STATIONARY; +} + +static inline std::string format_mac(uint8_t *buffer) { + return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], + buffer[15]); +} + +static inline std::string format_version(uint8_t *buffer) { + return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], + buffer[14]); +} + +LD2450Component::LD2450Component() {} + +void LD2450Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up HLK-LD2450..."); +#ifdef USE_NUMBER + this->pref_ = global_preferences->make_preference(this->presence_timeout_number_->get_object_id_hash()); + this->set_presence_timeout(); +#endif + this->read_all_info(); +} + +void LD2450Component::dump_config() { + ESP_LOGCONFIG(TAG, "HLK-LD2450 Human motion tracking radar module:"); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "StillTargetBinarySensor", this->still_target_binary_sensor_); +#endif +#ifdef USE_SWITCH + LOG_SWITCH(" ", "BluetoothSwitch", this->bluetooth_switch_); + LOG_SWITCH(" ", "MultiTargetSwitch", this->multi_target_switch_); +#endif +#ifdef USE_BUTTON + LOG_BUTTON(" ", "ResetButton", this->reset_button_); + LOG_BUTTON(" ", "RestartButton", this->restart_button_); +#endif +#ifdef USE_SENSOR + LOG_SENSOR(" ", "TargetCountSensor", this->target_count_sensor_); + LOG_SENSOR(" ", "StillTargetCountSensor", this->still_target_count_sensor_); + LOG_SENSOR(" ", "MovingTargetCountSensor", this->moving_target_count_sensor_); + for (sensor::Sensor *s : this->move_x_sensors_) { + LOG_SENSOR(" ", "NthTargetXSensor", s); + } + for (sensor::Sensor *s : this->move_y_sensors_) { + LOG_SENSOR(" ", "NthTargetYSensor", s); + } + for (sensor::Sensor *s : this->move_speed_sensors_) { + LOG_SENSOR(" ", "NthTargetSpeedSensor", s); + } + for (sensor::Sensor *s : this->move_angle_sensors_) { + LOG_SENSOR(" ", "NthTargetAngleSensor", s); + } + for (sensor::Sensor *s : this->move_distance_sensors_) { + LOG_SENSOR(" ", "NthTargetDistanceSensor", s); + } + for (sensor::Sensor *s : this->move_resolution_sensors_) { + LOG_SENSOR(" ", "NthTargetResolutionSensor", s); + } + for (sensor::Sensor *s : this->zone_target_count_sensors_) { + LOG_SENSOR(" ", "NthZoneTargetCountSensor", s); + } + for (sensor::Sensor *s : this->zone_still_target_count_sensors_) { + LOG_SENSOR(" ", "NthZoneStillTargetCountSensor", s); + } + for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) { + LOG_SENSOR(" ", "NthZoneMovingTargetCountSensor", s); + } +#endif +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "VersionTextSensor", this->version_text_sensor_); + LOG_TEXT_SENSOR(" ", "MacTextSensor", this->mac_text_sensor_); + for (text_sensor::TextSensor *s : this->direction_text_sensors_) { + LOG_TEXT_SENSOR(" ", "NthDirectionTextSensor", s); + } +#endif +#ifdef USE_NUMBER + for (number::Number *n : this->zone_x1_numbers_) { + LOG_NUMBER(" ", "ZoneX1Number", n); + } + for (number::Number *n : this->zone_y1_numbers_) { + LOG_NUMBER(" ", "ZoneY1Number", n); + } + for (number::Number *n : this->zone_x2_numbers_) { + LOG_NUMBER(" ", "ZoneX2Number", n); + } + for (number::Number *n : this->zone_y2_numbers_) { + LOG_NUMBER(" ", "ZoneY2Number", n); + } +#endif +#ifdef USE_SELECT + LOG_SELECT(" ", "BaudRateSelect", this->baud_rate_select_); + LOG_SELECT(" ", "ZoneTypeSelect", this->zone_type_select_); +#endif +#ifdef USE_NUMBER + LOG_NUMBER(" ", "PresenceTimeoutNumber", this->presence_timeout_number_); +#endif + ESP_LOGCONFIG(TAG, " Throttle : %ums", this->throttle_); + ESP_LOGCONFIG(TAG, " MAC Address : %s", const_cast(this->mac_.c_str())); + ESP_LOGCONFIG(TAG, " Firmware version : %s", const_cast(this->version_.c_str())); +} + +void LD2450Component::loop() { + while (this->available()) { + this->readline_(read(), this->buffer_data_, MAX_LINE_LENGTH); + } +} + +// Count targets in zone +uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving) { + uint8_t count = 0; + for (auto &index : this->target_info_) { + if (index.x > zone.x1 && index.x < zone.x2 && index.y > zone.y1 && index.y < zone.y2 && + index.is_moving == is_moving) { + count++; + } + } + return count; +} + +// Service reset_radar_zone +void LD2450Component::reset_radar_zone() { + this->zone_type_ = 0; + for (auto &i : this->zone_config_) { + i.x1 = 0; + i.y1 = 0; + i.x2 = 0; + i.y2 = 0; + } + this->send_set_zone_command_(); +} + +void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2, + int32_t zone1_y2, int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, + int32_t zone2_y2, int32_t zone3_x1, int32_t zone3_y1, int32_t zone3_x2, + int32_t zone3_y2) { + this->zone_type_ = zone_type; + int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1, + zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2}; + for (int i = 0; i < MAX_ZONES; i++) { + this->zone_config_[i].x1 = zone_parameters[i * 4]; + this->zone_config_[i].y1 = zone_parameters[i * 4 + 1]; + this->zone_config_[i].x2 = zone_parameters[i * 4 + 2]; + this->zone_config_[i].y2 = zone_parameters[i * 4 + 3]; + } + this->send_set_zone_command_(); +} + +// Set Zone on LD2450 Sensor +void LD2450Component::send_set_zone_command_() { + uint8_t cmd_value[26] = {}; + uint8_t zone_type_bytes[2] = {static_cast(this->zone_type_), 0x00}; + uint8_t area_config[24] = {}; + for (int i = 0; i < MAX_ZONES; i++) { + int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2, + this->zone_config_[i].y2}; + ld2450::convert_int_values_to_hex(values, area_config + (i * 8)); + } + std::memcpy(cmd_value, zone_type_bytes, 2); + std::memcpy(cmd_value + 2, area_config, 24); + this->set_config_mode_(true); + this->send_command_(CMD_SET_ZONE, cmd_value, 26); + this->set_config_mode_(false); +} + +// Check presense timeout to reset presence status +bool LD2450Component::get_timeout_status_(uint32_t check_millis) { + if (check_millis == 0) { + return true; + } + if (this->timeout_ == 0) { + this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT); + } + auto current_millis = millis(); + return current_millis - check_millis >= this->timeout_; +} + +// Extract, store and publish zone details LD2450 buffer +void LD2450Component::process_zone_(uint8_t *buffer) { + uint8_t index, start; + for (index = 0; index < MAX_ZONES; index++) { + start = 12 + index * 8; + this->zone_config_[index].x1 = ld2450::hex_to_signed_int(buffer, start); + this->zone_config_[index].y1 = ld2450::hex_to_signed_int(buffer, start + 2); + this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4); + this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6); +#ifdef USE_NUMBER + this->zone_x1_numbers_[index]->publish_state(this->zone_config_[index].x1); + this->zone_y1_numbers_[index]->publish_state(this->zone_config_[index].y1); + this->zone_x2_numbers_[index]->publish_state(this->zone_config_[index].x2); + this->zone_y2_numbers_[index]->publish_state(this->zone_config_[index].y2); +#endif + } +} + +// Read all info from LD2450 buffer +void LD2450Component::read_all_info() { + this->set_config_mode_(true); + this->get_version_(); + this->get_mac_(); + this->query_target_tracking_mode_(); + this->query_zone_(); + this->set_config_mode_(false); +#ifdef USE_SELECT + const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); + if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) { + this->baud_rate_select_->publish_state(baud_rate); + } + this->publish_zone_type(); +#endif +} + +// Read zone info from LD2450 buffer +void LD2450Component::query_zone_info() { + this->set_config_mode_(true); + this->query_zone_(); + this->set_config_mode_(false); +} + +// Restart LD2450 and read all info from buffer +void LD2450Component::restart_and_read_all_info() { + this->set_config_mode_(true); + this->restart_(); + this->set_timeout(1000, [this]() { this->read_all_info(); }); +} + +// Send command with values to LD2450 +void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { + ESP_LOGV(TAG, "Sending command %02X", command); + // frame header + this->write_array(CMD_FRAME_HEADER, 4); + // length bytes + int len = 2; + if (command_value != nullptr) { + len += command_value_len; + } + this->write_byte(lowbyte(len)); + this->write_byte(highbyte(len)); + // command + this->write_byte(lowbyte(command)); + this->write_byte(highbyte(command)); + // command value bytes + if (command_value != nullptr) { + for (int i = 0; i < command_value_len; i++) { + this->write_byte(command_value[i]); + } + } + // footer + this->write_array(CMD_FRAME_END, 4); + // FIXME to remove + delay(50); // NOLINT +} + +// LD2450 Radar data message: +// [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC] +// Header Target 1 Target 2 Target 3 End +void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { + if (len < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes) + ESP_LOGE(TAG, "Periodic data: invalid message length"); + return; + } + if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) { // header + ESP_LOGE(TAG, "Periodic data: invalid message header"); + return; + } + if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) { // footer + ESP_LOGE(TAG, "Periodic data: invalid message footer"); + return; + } + + auto current_millis = millis(); + if (current_millis - this->last_periodic_millis_ < this->throttle_) { + ESP_LOGV(TAG, "Throttling: %d", this->throttle_); + return; + } + + this->last_periodic_millis_ = current_millis; + + int16_t target_count = 0; + int16_t still_target_count = 0; + int16_t moving_target_count = 0; + int16_t start = 0; + int16_t val = 0; + uint8_t index = 0; + int16_t tx = 0; + int16_t ty = 0; + int16_t td = 0; + int16_t ts = 0; + int16_t angle = 0; + std::string direction{}; + bool is_moving = false; + +#ifdef USE_SENSOR + // Loop thru targets + // X + for (index = 0; index < MAX_TARGETS; index++) { + start = TARGET_X + index * 8; + is_moving = false; + sensor::Sensor *sx = this->move_x_sensors_[index]; + if (sx != nullptr) { + val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]); + tx = val; + sx->publish_state(val); + } + // Y + start = TARGET_Y + index * 8; + sensor::Sensor *sy = this->move_y_sensors_[index]; + if (sy != nullptr) { + val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]); + ty = val; + sy->publish_state(val); + } + // SPEED + start = TARGET_SPEED + index * 8; + sensor::Sensor *ss = this->move_speed_sensors_[index]; + if (ss != nullptr) { + val = ld2450::decode_speed(buffer[start], buffer[start + 1]); + ts = val; + if (val) { + is_moving = true; + moving_target_count++; + } + ss->publish_state(val); + } + // RESOLUTION + start = TARGET_RESOLUTION + index * 8; + sensor::Sensor *sr = this->move_resolution_sensors_[index]; + if (sr != nullptr) { + val = (buffer[start + 1] << 8) | buffer[start]; + sr->publish_state(val); + } + // DISTANCE + sensor::Sensor *sd = this->move_distance_sensors_[index]; + if (sd != nullptr) { + val = (uint16_t) sqrt( + pow(ld2450::decode_coordinate(buffer[TARGET_X + index * 8], buffer[(TARGET_X + index * 8) + 1]), 2) + + pow(ld2450::decode_coordinate(buffer[TARGET_Y + index * 8], buffer[(TARGET_Y + index * 8) + 1]), 2)); + td = val; + if (val > 0) { + target_count++; + } + + sd->publish_state(val); + } + // ANGLE + angle = calculate_angle(static_cast(ty), static_cast(td)); + if (tx > 0) { + angle = angle * -1; + } + sensor::Sensor *sa = this->move_angle_sensors_[index]; + if (sa != nullptr) { + sa->publish_state(angle); + } +#endif + // DIRECTION +#ifdef USE_TEXT_SENSOR + direction = get_direction(ts); + if (td == 0) { + direction = "NA"; + } + text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; + if (tsd != nullptr) { + tsd->publish_state(direction); + } +#endif + + // Store target info for zone target count + this->target_info_[index].x = tx; + this->target_info_[index].y = ty; + this->target_info_[index].is_moving = is_moving; + + } // End loop thru targets + +#ifdef USE_SENSOR + // Loop thru zones + uint8_t zone_still_targets = 0; + uint8_t zone_moving_targets = 0; + uint8_t zone_all_targets = 0; + for (index = 0; index < MAX_ZONES; index++) { + // Publish Still Target Count in Zones + sensor::Sensor *szstc = this->zone_still_target_count_sensors_[index]; + if (szstc != nullptr) { + zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false); + szstc->publish_state(zone_still_targets); + } + // Publish Moving Target Count in Zones + sensor::Sensor *szmtc = this->zone_moving_target_count_sensors_[index]; + if (szmtc != nullptr) { + zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true); + szmtc->publish_state(zone_moving_targets); + } + + zone_all_targets = zone_still_targets + zone_moving_targets; + + // Publish All Target Count in Zones + sensor::Sensor *sztc = this->zone_target_count_sensors_[index]; + if (sztc != nullptr) { + sztc->publish_state(zone_all_targets); + } + + } // End loop thru zones + + still_target_count = target_count - moving_target_count; + // Target Count + if (this->target_count_sensor_ != nullptr) { + this->target_count_sensor_->publish_state(target_count); + } + // Still Target Count + if (this->still_target_count_sensor_ != nullptr) { + this->still_target_count_sensor_->publish_state(still_target_count); + } + // Moving Target Count + if (this->moving_target_count_sensor_ != nullptr) { + this->moving_target_count_sensor_->publish_state(moving_target_count); + } +#endif + +#ifdef USE_BINARY_SENSOR + // Target Presence + if (this->target_binary_sensor_ != nullptr) { + if (target_count > 0) { + this->target_binary_sensor_->publish_state(true); + } else { + if (this->get_timeout_status_(this->presence_millis_)) { + this->target_binary_sensor_->publish_state(false); + } else { + ESP_LOGV(TAG, "Clear presence waiting timeout: %d", this->timeout_); + } + } + } + // Moving Target Presence + if (this->moving_target_binary_sensor_ != nullptr) { + if (moving_target_count > 0) { + this->moving_target_binary_sensor_->publish_state(true); + } else { + if (this->get_timeout_status_(this->moving_presence_millis_)) { + this->moving_target_binary_sensor_->publish_state(false); + } + } + } + // Still Target Presence + if (this->still_target_binary_sensor_ != nullptr) { + if (still_target_count > 0) { + this->still_target_binary_sensor_->publish_state(true); + } else { + if (this->get_timeout_status_(this->still_presence_millis_)) { + this->still_target_binary_sensor_->publish_state(false); + } + } + } +#endif +#ifdef USE_SENSOR + // For presence timeout check + if (target_count > 0) { + this->presence_millis_ = millis(); + } + if (moving_target_count > 0) { + this->moving_presence_millis_ = millis(); + } + if (still_target_count > 0) { + this->still_presence_millis_ = millis(); + } +#endif +} + +bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { + ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]); + if (len < 10) { + ESP_LOGE(TAG, "Ack data: invalid length"); + return true; + } + if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // frame header + ESP_LOGE(TAG, "Ack data: invalid header (command %02X)", buffer[COMMAND]); + return true; + } + if (buffer[COMMAND_STATUS] != 0x01) { + ESP_LOGE(TAG, "Ack data: invalid status"); + return true; + } + if (buffer[8] || buffer[9]) { + ESP_LOGE(TAG, "Ack data: last buffer was %u, %u", buffer[8], buffer[9]); + return true; + } + + switch (buffer[COMMAND]) { + case lowbyte(CMD_ENABLE_CONF): + ESP_LOGV(TAG, "Got enable conf command"); + break; + case lowbyte(CMD_DISABLE_CONF): + ESP_LOGV(TAG, "Got disable conf command"); + break; + case lowbyte(CMD_SET_BAUD_RATE): + ESP_LOGV(TAG, "Got baud rate change command"); +#ifdef USE_SELECT + if (this->baud_rate_select_ != nullptr) { + ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str()); + } +#endif + break; + case lowbyte(CMD_VERSION): + this->version_ = ld2450::format_version(buffer); + ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); +#ifdef USE_TEXT_SENSOR + if (this->version_text_sensor_ != nullptr) { + this->version_text_sensor_->publish_state(this->version_); + } +#endif + break; + case lowbyte(CMD_MAC): + if (len < 20) { + return false; + } + this->mac_ = ld2450::format_mac(buffer); + ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); +#ifdef USE_TEXT_SENSOR + if (this->mac_text_sensor_ != nullptr) { + this->mac_text_sensor_->publish_state(this->mac_); + } +#endif +#ifdef USE_SWITCH + if (this->bluetooth_switch_ != nullptr) { + this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); + } +#endif + break; + case lowbyte(CMD_BLUETOOTH): + ESP_LOGV(TAG, "Got Bluetooth command"); + break; + case lowbyte(CMD_SINGLE_TARGET_MODE): + ESP_LOGV(TAG, "Got single target conf command"); +#ifdef USE_SWITCH + if (this->multi_target_switch_ != nullptr) { + this->multi_target_switch_->publish_state(false); + } +#endif + break; + case lowbyte(CMD_MULTI_TARGET_MODE): + ESP_LOGV(TAG, "Got multi target conf command"); +#ifdef USE_SWITCH + if (this->multi_target_switch_ != nullptr) { + this->multi_target_switch_->publish_state(true); + } +#endif + break; + case lowbyte(CMD_QUERY_TARGET_MODE): + ESP_LOGV(TAG, "Got query target tracking mode command"); +#ifdef USE_SWITCH + if (this->multi_target_switch_ != nullptr) { + this->multi_target_switch_->publish_state(buffer[10] == 0x02); + } +#endif + break; + case lowbyte(CMD_QUERY_ZONE): + ESP_LOGV(TAG, "Got query zone conf command"); + this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16); + this->publish_zone_type(); +#ifdef USE_SELECT + if (this->zone_type_select_ != nullptr) { + ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str()); + } +#endif + if (buffer[10] == 0x00) { + ESP_LOGV(TAG, "Zone: Disabled"); + } + if (buffer[10] == 0x01) { + ESP_LOGV(TAG, "Zone: Area detection"); + } + if (buffer[10] == 0x02) { + ESP_LOGV(TAG, "Zone: Area filter"); + } + this->process_zone_(buffer); + break; + case lowbyte(CMD_SET_ZONE): + ESP_LOGV(TAG, "Got set zone conf command"); + this->query_zone_info(); + break; + default: + break; + } + return true; +} + +// Read LD2450 buffer data +void LD2450Component::readline_(int readch, uint8_t *buffer, uint8_t len) { + if (readch < 0) { + return; + } + if (this->buffer_pos_ < len - 1) { + buffer[this->buffer_pos_++] = readch; + buffer[this->buffer_pos_] = 0; + } else { + this->buffer_pos_ = 0; + } + if (this->buffer_pos_ < 4) { + return; + } + if (buffer[this->buffer_pos_ - 2] == 0x55 && buffer[this->buffer_pos_ - 1] == 0xCC) { + ESP_LOGV(TAG, "Handle periodic radar data"); + this->handle_periodic_data_(buffer, this->buffer_pos_); + this->buffer_pos_ = 0; // Reset position index for next frame + } else if (buffer[this->buffer_pos_ - 4] == 0x04 && buffer[this->buffer_pos_ - 3] == 0x03 && + buffer[this->buffer_pos_ - 2] == 0x02 && buffer[this->buffer_pos_ - 1] == 0x01) { + ESP_LOGV(TAG, "Handle command ack data"); + if (this->handle_ack_data_(buffer, this->buffer_pos_)) { + this->buffer_pos_ = 0; // Reset position index for next frame + } else { + ESP_LOGV(TAG, "Command ack data invalid"); + } + } +} + +// Set Config Mode - Pre-requisite sending commands +void LD2450Component::set_config_mode_(bool enable) { + uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; + uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(cmd, enable ? cmd_value : nullptr, 2); +} + +// Set Bluetooth Enable/Disable +void LD2450Component::set_bluetooth(bool enable) { + this->set_config_mode_(true); + uint8_t enable_cmd_value[2] = {0x01, 0x00}; + uint8_t disable_cmd_value[2] = {0x00, 0x00}; + this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +// Set Baud rate +void LD2450Component::set_baud_rate(const std::string &state) { + this->set_config_mode_(true); + uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; + this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); + this->set_timeout(200, [this]() { this->restart_(); }); +} + +// Set Zone Type - one of: Disabled, Detection, Filter +void LD2450Component::set_zone_type(const std::string &state) { + ESP_LOGV(TAG, "Set zone type: %s", state.c_str()); + uint8_t zone_type = ZONE_TYPE_ENUM_TO_INT.at(state); + this->zone_type_ = zone_type; + this->send_set_zone_command_(); +} + +// Publish Zone Type to Select component +void LD2450Component::publish_zone_type() { +#ifdef USE_SELECT + std::string zone_type = ZONE_TYPE_INT_TO_ENUM.at(static_cast(this->zone_type_)); + if (this->zone_type_select_ != nullptr) { + this->zone_type_select_->publish_state(zone_type); + } +#endif +} + +// Set Single/Multiplayer target detection +void LD2450Component::set_multi_target(bool enable) { + this->set_config_mode_(true); + uint8_t cmd = enable ? CMD_MULTI_TARGET_MODE : CMD_SINGLE_TARGET_MODE; + this->send_command_(cmd, nullptr, 0); + this->set_config_mode_(false); +} + +// LD2450 factory reset +void LD2450Component::factory_reset() { + this->set_config_mode_(true); + this->send_command_(CMD_RESET, nullptr, 0); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +// Restart LD2450 module +void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } + +// Get LD2450 firmware version +void LD2450Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); } + +// Get LD2450 mac address +void LD2450Component::get_mac_() { + uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(CMD_MAC, cmd_value, 2); +} + +// Query for target tracking mode +void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QUERY_TARGET_MODE, nullptr, 0); } + +// Query for zone info +void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); } + +#ifdef USE_SENSOR +void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) { this->move_x_sensors_[target] = s; } +void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) { this->move_y_sensors_[target] = s; } +void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) { + this->move_speed_sensors_[target] = s; +} +void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) { + this->move_angle_sensors_[target] = s; +} +void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) { + this->move_distance_sensors_[target] = s; +} +void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) { + this->move_resolution_sensors_[target] = s; +} +void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) { + this->zone_target_count_sensors_[zone] = s; +} +void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) { + this->zone_still_target_count_sensors_[zone] = s; +} +void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) { + this->zone_moving_target_count_sensors_[zone] = s; +} +#endif +#ifdef USE_TEXT_SENSOR +void LD2450Component::set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s) { + this->direction_text_sensors_[target] = s; +} +#endif + +// Send Zone coordinates data to LD2450 +#ifdef USE_NUMBER +void LD2450Component::set_zone_coordinate(uint8_t zone) { + number::Number *x1sens = this->zone_x1_numbers_[zone]; + number::Number *y1sens = this->zone_y1_numbers_[zone]; + number::Number *x2sens = this->zone_x2_numbers_[zone]; + number::Number *y2sens = this->zone_y2_numbers_[zone]; + if (!x1sens->has_state() || !y1sens->has_state() || !x2sens->has_state() || !y2sens->has_state()) { + return; + } + this->zone_config_[zone].x1 = static_cast(x1sens->state); + this->zone_config_[zone].y1 = static_cast(y1sens->state); + this->zone_config_[zone].x2 = static_cast(x2sens->state); + this->zone_config_[zone].y2 = static_cast(y2sens->state); + this->send_set_zone_command_(); +} + +void LD2450Component::set_zone_x1_number(uint8_t zone, number::Number *n) { this->zone_x1_numbers_[zone] = n; } +void LD2450Component::set_zone_y1_number(uint8_t zone, number::Number *n) { this->zone_y1_numbers_[zone] = n; } +void LD2450Component::set_zone_x2_number(uint8_t zone, number::Number *n) { this->zone_x2_numbers_[zone] = n; } +void LD2450Component::set_zone_y2_number(uint8_t zone, number::Number *n) { this->zone_y2_numbers_[zone] = n; } +#endif + +// Set Presence Timeout load and save from flash +#ifdef USE_NUMBER +void LD2450Component::set_presence_timeout() { + if (this->presence_timeout_number_ != nullptr) { + if (this->presence_timeout_number_->state == 0) { + float timeout = this->restore_from_flash_(); + this->presence_timeout_number_->publish_state(timeout); + this->timeout_ = ld2450::convert_seconds_to_ms(timeout); + } + if (this->presence_timeout_number_->has_state()) { + this->save_to_flash_(this->presence_timeout_number_->state); + this->timeout_ = ld2450::convert_seconds_to_ms(this->presence_timeout_number_->state); + } + } +} + +// Save Presence Timeout to flash +void LD2450Component::save_to_flash_(float value) { this->pref_.save(&value); } + +// Load Presence Timeout from flash +float LD2450Component::restore_from_flash_() { + float value; + if (!this->pref_.load(&value)) { + value = DEFAULT_PRESENCE_TIMEOUT; + } + return value; +} +#endif + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h new file mode 100644 index 0000000000..2fed7dc0c9 --- /dev/null +++ b/esphome/components/ld2450/ld2450.h @@ -0,0 +1,231 @@ +#pragma once + +#include +#include +#include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#ifndef M_PI +#define M_PI 3.14 +#endif + +namespace esphome { +namespace ld2450 { + +// Constants +static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. +static const uint8_t MAX_LINE_LENGTH = 60; // Max characters for serial buffer +static const uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 +static const uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 + +// Target coordinate struct +struct Target { + int16_t x; + int16_t y; + bool is_moving; +}; + +// Zone coordinate struct +struct Zone { + int16_t x1 = 0; + int16_t y1 = 0; + int16_t x2 = 0; + int16_t y2 = 0; +}; + +enum BaudRateStructure : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8 +}; + +// Convert baud rate enum to int +static const std::map BAUD_RATE_ENUM_TO_INT{ + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; + +// Zone type struct +enum ZoneTypeStructure : uint8_t { ZONE_DISABLED = 0, ZONE_DETECTION = 1, ZONE_FILTER = 2 }; + +// Convert zone type int to enum +static const std::map ZONE_TYPE_INT_TO_ENUM{ + {ZONE_DISABLED, "Disabled"}, {ZONE_DETECTION, "Detection"}, {ZONE_FILTER, "Filter"}}; + +// Convert zone type enum to int +static const std::map ZONE_TYPE_ENUM_TO_INT{ + {"Disabled", ZONE_DISABLED}, {"Detection", ZONE_DETECTION}, {"Filter", ZONE_FILTER}}; + +// LD2450 serial command header & footer +static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; +static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; + +enum PeriodicDataStructure : uint8_t { + TARGET_X = 4, + TARGET_Y = 6, + TARGET_SPEED = 8, + TARGET_RESOLUTION = 10, +}; + +enum PeriodicDataValue : uint8_t { HEAD = 0XAA, END = 0x55, CHECK = 0x00 }; + +enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; + +class LD2450Component : public Component, public uart::UARTDevice { +#ifdef USE_SENSOR + SUB_SENSOR(target_count) + SUB_SENSOR(still_target_count) + SUB_SENSOR(moving_target_count) +#endif +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(target) + SUB_BINARY_SENSOR(moving_target) + SUB_BINARY_SENSOR(still_target) +#endif +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(version) + SUB_TEXT_SENSOR(mac) +#endif +#ifdef USE_SELECT + SUB_SELECT(baud_rate) + SUB_SELECT(zone_type) +#endif +#ifdef USE_SWITCH + SUB_SWITCH(bluetooth) + SUB_SWITCH(multi_target) +#endif +#ifdef USE_BUTTON + SUB_BUTTON(reset) + SUB_BUTTON(restart) +#endif +#ifdef USE_NUMBER + SUB_NUMBER(presence_timeout) +#endif + + public: + LD2450Component(); + void setup() override; + void dump_config() override; + void loop() override; + void set_presence_timeout(); + void set_throttle(uint16_t value) { this->throttle_ = value; }; + void read_all_info(); + void query_zone_info(); + void restart_and_read_all_info(); + void set_bluetooth(bool enable); + void set_multi_target(bool enable); + void set_baud_rate(const std::string &state); + void set_zone_type(const std::string &state); + void publish_zone_type(); + void factory_reset(); +#ifdef USE_TEXT_SENSOR + void set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s); +#endif +#ifdef USE_NUMBER + void set_zone_coordinate(uint8_t zone); + void set_zone_x1_number(uint8_t zone, number::Number *n); + void set_zone_y1_number(uint8_t zone, number::Number *n); + void set_zone_x2_number(uint8_t zone, number::Number *n); + void set_zone_y2_number(uint8_t zone, number::Number *n); +#endif +#ifdef USE_SENSOR + void set_move_x_sensor(uint8_t target, sensor::Sensor *s); + void set_move_y_sensor(uint8_t target, sensor::Sensor *s); + void set_move_speed_sensor(uint8_t target, sensor::Sensor *s); + void set_move_angle_sensor(uint8_t target, sensor::Sensor *s); + void set_move_distance_sensor(uint8_t target, sensor::Sensor *s); + void set_move_resolution_sensor(uint8_t target, sensor::Sensor *s); + void set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s); + void set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s); + void set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s); +#endif + void reset_radar_zone(); + void set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2, int32_t zone1_y2, + int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1, + int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2); + + protected: + void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); + void set_config_mode_(bool enable); + void handle_periodic_data_(uint8_t *buffer, uint8_t len); + bool handle_ack_data_(uint8_t *buffer, uint8_t len); + void process_zone_(uint8_t *buffer); + void readline_(int readch, uint8_t *buffer, uint8_t len); + void get_version_(); + void get_mac_(); + void query_target_tracking_mode_(); + void query_zone_(); + void restart_(); + void send_set_zone_command_(); + void save_to_flash_(float value); + float restore_from_flash_(); + bool get_timeout_status_(uint32_t check_millis); + uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); + + Target target_info_[MAX_TARGETS]; + Zone zone_config_[MAX_ZONES]; + uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer + uint8_t buffer_data_[MAX_LINE_LENGTH]; + uint32_t last_periodic_millis_ = 0; + uint32_t presence_millis_ = 0; + uint32_t still_presence_millis_ = 0; + uint32_t moving_presence_millis_ = 0; + uint16_t throttle_ = 0; + uint16_t timeout_ = 5; + uint8_t zone_type_ = 0; + std::string version_{}; + std::string mac_{}; +#ifdef USE_NUMBER + ESPPreferenceObject pref_; // only used when numbers are in use + std::vector zone_x1_numbers_ = std::vector(MAX_ZONES); + std::vector zone_y1_numbers_ = std::vector(MAX_ZONES); + std::vector zone_x2_numbers_ = std::vector(MAX_ZONES); + std::vector zone_y2_numbers_ = std::vector(MAX_ZONES); +#endif +#ifdef USE_SENSOR + std::vector move_x_sensors_ = std::vector(MAX_TARGETS); + std::vector move_y_sensors_ = std::vector(MAX_TARGETS); + std::vector move_speed_sensors_ = std::vector(MAX_TARGETS); + std::vector move_angle_sensors_ = std::vector(MAX_TARGETS); + std::vector move_distance_sensors_ = std::vector(MAX_TARGETS); + std::vector move_resolution_sensors_ = std::vector(MAX_TARGETS); + std::vector zone_target_count_sensors_ = std::vector(MAX_ZONES); + std::vector zone_still_target_count_sensors_ = std::vector(MAX_ZONES); + std::vector zone_moving_target_count_sensors_ = std::vector(MAX_ZONES); +#endif +#ifdef USE_TEXT_SENSOR + std::vector direction_text_sensors_ = std::vector(3); +#endif +}; + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/number/__init__.py b/esphome/components/ld2450/number/__init__.py new file mode 100644 index 0000000000..8e83de56a0 --- /dev/null +++ b/esphome/components/ld2450/number/__init__.py @@ -0,0 +1,120 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_DISTANCE, + ENTITY_CATEGORY_CONFIG, + ICON_TIMELAPSE, + UNIT_MILLIMETER, + UNIT_SECOND, +) + +from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns + +CONF_PRESENCE_TIMEOUT = "presence_timeout" +CONF_X1 = "x1" +CONF_X2 = "x2" +CONF_Y1 = "y1" +CONF_Y2 = "y2" +ICON_ARROW_BOTTOM_RIGHT = "mdi:arrow-bottom-right" +ICON_ARROW_BOTTOM_RIGHT_BOLD_BOX_OUTLINE = "mdi:arrow-bottom-right-bold-box-outline" +ICON_ARROW_TOP_LEFT = "mdi:arrow-top-left" +ICON_ARROW_TOP_LEFT_BOLD_BOX_OUTLINE = "mdi:arrow-top-left-bold-box-outline" +MAX_ZONES = 3 + +PresenceTimeoutNumber = ld2450_ns.class_("PresenceTimeoutNumber", number.Number) +ZoneCoordinateNumber = ld2450_ns.class_("ZoneCoordinateNumber", number.Number) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), + cv.Required(CONF_PRESENCE_TIMEOUT): number.number_schema( + PresenceTimeoutNumber, + unit_of_measurement=UNIT_SECOND, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_TIMELAPSE, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"zone_{n + 1}"): cv.Schema( + { + cv.Required(CONF_X1): number.number_schema( + ZoneCoordinateNumber, + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_MILLIMETER, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_ARROW_TOP_LEFT_BOLD_BOX_OUTLINE, + ), + cv.Required(CONF_Y1): number.number_schema( + ZoneCoordinateNumber, + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_MILLIMETER, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_ARROW_TOP_LEFT, + ), + cv.Required(CONF_X2): number.number_schema( + ZoneCoordinateNumber, + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_MILLIMETER, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_ARROW_BOTTOM_RIGHT_BOLD_BOX_OUTLINE, + ), + cv.Required(CONF_Y2): number.number_schema( + ZoneCoordinateNumber, + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_MILLIMETER, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_ARROW_BOTTOM_RIGHT, + ), + } + ) + for n in range(MAX_ZONES) + } +) + + +async def to_code(config): + ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) + if presence_timeout_config := config.get(CONF_PRESENCE_TIMEOUT): + n = await number.new_number( + presence_timeout_config, + min_value=0, + max_value=3600, + step=1, + ) + await cg.register_parented(n, config[CONF_LD2450_ID]) + cg.add(ld2450_component.set_presence_timeout_number(n)) + for x in range(MAX_ZONES): + if zone_conf := config.get(f"zone_{x + 1}"): + if zone_x1_config := zone_conf.get(CONF_X1): + n = cg.new_Pvariable(zone_x1_config[CONF_ID], x) + await number.register_number( + n, zone_x1_config, min_value=-4860, max_value=4860, step=1 + ) + await cg.register_parented(n, config[CONF_LD2450_ID]) + cg.add(ld2450_component.set_zone_x1_number(x, n)) + if zone_y1_config := zone_conf.get(CONF_Y1): + n = cg.new_Pvariable(zone_y1_config[CONF_ID], x) + await number.register_number( + n, zone_y1_config, min_value=0, max_value=7560, step=1 + ) + await cg.register_parented(n, config[CONF_LD2450_ID]) + cg.add(ld2450_component.set_zone_y1_number(x, n)) + if zone_x2_config := zone_conf.get(CONF_X2): + n = cg.new_Pvariable(zone_x2_config[CONF_ID], x) + await number.register_number( + n, zone_x2_config, min_value=-4860, max_value=4860, step=1 + ) + await cg.register_parented(n, config[CONF_LD2450_ID]) + cg.add(ld2450_component.set_zone_x2_number(x, n)) + if zone_y2_config := zone_conf.get(CONF_Y2): + n = cg.new_Pvariable(zone_y2_config[CONF_ID], x) + await number.register_number( + n, zone_y2_config, min_value=0, max_value=7560, step=1 + ) + await cg.register_parented(n, config[CONF_LD2450_ID]) + cg.add(ld2450_component.set_zone_y2_number(x, n)) diff --git a/esphome/components/ld2450/number/presence_timeout_number.cpp b/esphome/components/ld2450/number/presence_timeout_number.cpp new file mode 100644 index 0000000000..ecfe71f484 --- /dev/null +++ b/esphome/components/ld2450/number/presence_timeout_number.cpp @@ -0,0 +1,12 @@ +#include "presence_timeout_number.h" + +namespace esphome { +namespace ld2450 { + +void PresenceTimeoutNumber::control(float value) { + this->publish_state(value); + this->parent_->set_presence_timeout(); +} + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/number/presence_timeout_number.h b/esphome/components/ld2450/number/presence_timeout_number.h new file mode 100644 index 0000000000..b18699792f --- /dev/null +++ b/esphome/components/ld2450/number/presence_timeout_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2450.h" + +namespace esphome { +namespace ld2450 { + +class PresenceTimeoutNumber : public number::Number, public Parented { + public: + PresenceTimeoutNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/number/zone_coordinate_number.cpp b/esphome/components/ld2450/number/zone_coordinate_number.cpp new file mode 100644 index 0000000000..5338d7e5ee --- /dev/null +++ b/esphome/components/ld2450/number/zone_coordinate_number.cpp @@ -0,0 +1,14 @@ +#include "zone_coordinate_number.h" + +namespace esphome { +namespace ld2450 { + +ZoneCoordinateNumber::ZoneCoordinateNumber(uint8_t zone) : zone_(zone) {} + +void ZoneCoordinateNumber::control(float value) { + this->publish_state(value); + this->parent_->set_zone_coordinate(this->zone_); +} + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/number/zone_coordinate_number.h b/esphome/components/ld2450/number/zone_coordinate_number.h new file mode 100644 index 0000000000..72b83889c4 --- /dev/null +++ b/esphome/components/ld2450/number/zone_coordinate_number.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2450.h" + +namespace esphome { +namespace ld2450 { + +class ZoneCoordinateNumber : public number::Number, public Parented { + public: + ZoneCoordinateNumber(uint8_t zone); + + protected: + uint8_t zone_; + void control(float value) override; +}; + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/select/__init__.py b/esphome/components/ld2450/select/__init__.py new file mode 100644 index 0000000000..25dd819637 --- /dev/null +++ b/esphome/components/ld2450/select/__init__.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import CONF_BAUD_RATE, ENTITY_CATEGORY_CONFIG, ICON_THERMOMETER + +from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns + +CONF_ZONE_TYPE = "zone_type" + +BaudRateSelect = ld2450_ns.class_("BaudRateSelect", select.Select) +ZoneTypeSelect = ld2450_ns.class_("ZoneTypeSelect", select.Select) + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), + cv.Optional(CONF_BAUD_RATE): select.select_schema( + BaudRateSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_THERMOMETER, + ), + cv.Optional(CONF_ZONE_TYPE): select.select_schema( + ZoneTypeSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_THERMOMETER, + ), +} + + +async def to_code(config): + ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) + if baud_rate_config := config.get(CONF_BAUD_RATE): + s = await select.new_select( + baud_rate_config, + options=[ + "9600", + "19200", + "38400", + "57600", + "115200", + "230400", + "256000", + "460800", + ], + ) + await cg.register_parented(s, config[CONF_LD2450_ID]) + cg.add(ld2450_component.set_baud_rate_select(s)) + if zone_type_config := config.get(CONF_ZONE_TYPE): + s = await select.new_select( + zone_type_config, + options=[ + "Disabled", + "Detection", + "Filter", + ], + ) + await cg.register_parented(s, config[CONF_LD2450_ID]) + cg.add(ld2450_component.set_zone_type_select(s)) diff --git a/esphome/components/ld2450/select/baud_rate_select.cpp b/esphome/components/ld2450/select/baud_rate_select.cpp new file mode 100644 index 0000000000..06439aaa75 --- /dev/null +++ b/esphome/components/ld2450/select/baud_rate_select.cpp @@ -0,0 +1,12 @@ +#include "baud_rate_select.h" + +namespace esphome { +namespace ld2450 { + +void BaudRateSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_baud_rate(state); +} + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/select/baud_rate_select.h b/esphome/components/ld2450/select/baud_rate_select.h new file mode 100644 index 0000000000..04fe65b4fd --- /dev/null +++ b/esphome/components/ld2450/select/baud_rate_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2450.h" + +namespace esphome { +namespace ld2450 { + +class BaudRateSelect : public select::Select, public Parented { + public: + BaudRateSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/select/zone_type_select.cpp b/esphome/components/ld2450/select/zone_type_select.cpp new file mode 100644 index 0000000000..a9f6155142 --- /dev/null +++ b/esphome/components/ld2450/select/zone_type_select.cpp @@ -0,0 +1,12 @@ +#include "zone_type_select.h" + +namespace esphome { +namespace ld2450 { + +void ZoneTypeSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_zone_type(state); +} + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/select/zone_type_select.h b/esphome/components/ld2450/select/zone_type_select.h new file mode 100644 index 0000000000..8aafeb6beb --- /dev/null +++ b/esphome/components/ld2450/select/zone_type_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2450.h" + +namespace esphome { +namespace ld2450 { + +class ZoneTypeSelect : public select::Select, public Parented { + public: + ZoneTypeSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/sensor.py b/esphome/components/ld2450/sensor.py new file mode 100644 index 0000000000..21580c5801 --- /dev/null +++ b/esphome/components/ld2450/sensor.py @@ -0,0 +1,156 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ANGLE, + CONF_DISTANCE, + CONF_RESOLUTION, + CONF_SPEED, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_SPEED, + UNIT_DEGREES, + UNIT_MILLIMETER, +) + +from . import CONF_LD2450_ID, LD2450Component + +DEPENDENCIES = ["ld2450"] + +CONF_MOVING_TARGET_COUNT = "moving_target_count" +CONF_STILL_TARGET_COUNT = "still_target_count" +CONF_TARGET_COUNT = "target_count" +CONF_X = "x" +CONF_Y = "y" + +ICON_ACCOUNT_GROUP = "mdi:account-group" +ICON_ACCOUNT_SWITCH = "mdi:account-switch" +ICON_ALPHA_X_BOX_OUTLINE = "mdi:alpha-x-box-outline" +ICON_ALPHA_Y_BOX_OUTLINE = "mdi:alpha-y-box-outline" +ICON_FORMAT_TEXT_ROTATION_ANGLE_UP = "mdi:format-text-rotation-angle-up" +ICON_HUMAN_GREETING_PROXIMITY = "mdi:human-greeting-proximity" +ICON_MAP_MARKER_ACCOUNT = "mdi:map-marker-account" +ICON_MAP_MARKER_DISTANCE = "mdi:map-marker-distance" +ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE = "mdi:relation-zero-or-one-to-zero-or-one" +ICON_SPEEDOMETER_SLOW = "mdi:speedometer-slow" + +MAX_TARGETS = 3 +MAX_ZONES = 3 + +UNIT_MILLIMETER_PER_SECOND = "mm/s" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), + cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( + icon=ICON_ACCOUNT_GROUP, + ), + cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( + icon=ICON_HUMAN_GREETING_PROXIMITY, + ), + cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( + icon=ICON_ACCOUNT_SWITCH, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"target_{n + 1}"): cv.Schema( + { + cv.Optional(CONF_X): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_MILLIMETER, + icon=ICON_ALPHA_X_BOX_OUTLINE, + ), + cv.Optional(CONF_Y): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_MILLIMETER, + icon=ICON_ALPHA_Y_BOX_OUTLINE, + ), + cv.Optional(CONF_SPEED): sensor.sensor_schema( + device_class=DEVICE_CLASS_SPEED, + unit_of_measurement=UNIT_MILLIMETER_PER_SECOND, + icon=ICON_SPEEDOMETER_SLOW, + ), + cv.Optional(CONF_ANGLE): sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, + icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP, + ), + cv.Optional(CONF_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_MILLIMETER, + icon=ICON_MAP_MARKER_DISTANCE, + ), + cv.Optional(CONF_RESOLUTION): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_MILLIMETER, + icon=ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE, + ), + } + ) + for n in range(MAX_TARGETS) + }, + { + cv.Optional(f"zone_{n + 1}"): cv.Schema( + { + cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( + icon=ICON_MAP_MARKER_ACCOUNT, + ), + cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( + icon=ICON_MAP_MARKER_ACCOUNT, + ), + cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( + icon=ICON_MAP_MARKER_ACCOUNT, + ), + } + ) + for n in range(MAX_ZONES) + }, +) + + +async def to_code(config): + ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) + + if target_count_config := config.get(CONF_TARGET_COUNT): + sens = await sensor.new_sensor(target_count_config) + cg.add(ld2450_component.set_target_count_sensor(sens)) + + if still_target_count_config := config.get(CONF_STILL_TARGET_COUNT): + sens = await sensor.new_sensor(still_target_count_config) + cg.add(ld2450_component.set_still_target_count_sensor(sens)) + + if moving_target_count_config := config.get(CONF_MOVING_TARGET_COUNT): + sens = await sensor.new_sensor(moving_target_count_config) + cg.add(ld2450_component.set_moving_target_count_sensor(sens)) + for n in range(MAX_TARGETS): + if target_conf := config.get(f"target_{n + 1}"): + if x_config := target_conf.get(CONF_X): + sens = await sensor.new_sensor(x_config) + cg.add(ld2450_component.set_move_x_sensor(n, sens)) + if y_config := target_conf.get(CONF_Y): + sens = await sensor.new_sensor(y_config) + cg.add(ld2450_component.set_move_y_sensor(n, sens)) + if speed_config := target_conf.get(CONF_SPEED): + sens = await sensor.new_sensor(speed_config) + cg.add(ld2450_component.set_move_speed_sensor(n, sens)) + if angle_config := target_conf.get(CONF_ANGLE): + sens = await sensor.new_sensor(angle_config) + cg.add(ld2450_component.set_move_angle_sensor(n, sens)) + if distance_config := target_conf.get(CONF_DISTANCE): + sens = await sensor.new_sensor(distance_config) + cg.add(ld2450_component.set_move_distance_sensor(n, sens)) + if resolution_config := target_conf.get(CONF_RESOLUTION): + sens = await sensor.new_sensor(resolution_config) + cg.add(ld2450_component.set_move_resolution_sensor(n, sens)) + for n in range(MAX_ZONES): + if zone_config := config.get(f"zone_{n + 1}"): + if target_count_config := zone_config.get(CONF_TARGET_COUNT): + sens = await sensor.new_sensor(target_count_config) + cg.add(ld2450_component.set_zone_target_count_sensor(n, sens)) + if still_target_count_config := zone_config.get(CONF_STILL_TARGET_COUNT): + sens = await sensor.new_sensor(still_target_count_config) + cg.add(ld2450_component.set_zone_still_target_count_sensor(n, sens)) + if moving_target_count_config := zone_config.get(CONF_MOVING_TARGET_COUNT): + sens = await sensor.new_sensor(moving_target_count_config) + cg.add(ld2450_component.set_zone_moving_target_count_sensor(n, sens)) diff --git a/esphome/components/ld2450/switch/__init__.py b/esphome/components/ld2450/switch/__init__.py new file mode 100644 index 0000000000..fb3969cf50 --- /dev/null +++ b/esphome/components/ld2450/switch/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_SWITCH, + ENTITY_CATEGORY_CONFIG, + ICON_BLUETOOTH, + ICON_PULSE, +) + +from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns + +BluetoothSwitch = ld2450_ns.class_("BluetoothSwitch", switch.Switch) +MultiTargetSwitch = ld2450_ns.class_("MultiTargetSwitch", switch.Switch) + +CONF_BLUETOOTH = "bluetooth" +CONF_MULTI_TARGET = "multi_target" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), + cv.Optional(CONF_BLUETOOTH): switch.switch_schema( + BluetoothSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_BLUETOOTH, + ), + cv.Optional(CONF_MULTI_TARGET): switch.switch_schema( + MultiTargetSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_PULSE, + ), +} + + +async def to_code(config): + ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) + if bluetooth_config := config.get(CONF_BLUETOOTH): + s = await switch.new_switch(bluetooth_config) + await cg.register_parented(s, config[CONF_LD2450_ID]) + cg.add(ld2450_component.set_bluetooth_switch(s)) + if multi_target_config := config.get(CONF_MULTI_TARGET): + s = await switch.new_switch(multi_target_config) + await cg.register_parented(s, config[CONF_LD2450_ID]) + cg.add(ld2450_component.set_multi_target_switch(s)) diff --git a/esphome/components/ld2450/switch/bluetooth_switch.cpp b/esphome/components/ld2450/switch/bluetooth_switch.cpp new file mode 100644 index 0000000000..fa0d4fb06a --- /dev/null +++ b/esphome/components/ld2450/switch/bluetooth_switch.cpp @@ -0,0 +1,12 @@ +#include "bluetooth_switch.h" + +namespace esphome { +namespace ld2450 { + +void BluetoothSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_bluetooth(state); +} + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/switch/bluetooth_switch.h b/esphome/components/ld2450/switch/bluetooth_switch.h new file mode 100644 index 0000000000..3c1c4f755c --- /dev/null +++ b/esphome/components/ld2450/switch/bluetooth_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2450.h" + +namespace esphome { +namespace ld2450 { + +class BluetoothSwitch : public switch_::Switch, public Parented { + public: + BluetoothSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/switch/multi_target_switch.cpp b/esphome/components/ld2450/switch/multi_target_switch.cpp new file mode 100644 index 0000000000..a163e29fc5 --- /dev/null +++ b/esphome/components/ld2450/switch/multi_target_switch.cpp @@ -0,0 +1,12 @@ +#include "multi_target_switch.h" + +namespace esphome { +namespace ld2450 { + +void MultiTargetSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_multi_target(state); +} + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/switch/multi_target_switch.h b/esphome/components/ld2450/switch/multi_target_switch.h new file mode 100644 index 0000000000..ca6253588d --- /dev/null +++ b/esphome/components/ld2450/switch/multi_target_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2450.h" + +namespace esphome { +namespace ld2450 { + +class MultiTargetSwitch : public switch_::Switch, public Parented { + public: + MultiTargetSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/text_sensor.py b/esphome/components/ld2450/text_sensor.py new file mode 100644 index 0000000000..6c11024b89 --- /dev/null +++ b/esphome/components/ld2450/text_sensor.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_DIRECTION, + CONF_MAC_ADDRESS, + CONF_VERSION, + ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_NONE, + ICON_BLUETOOTH, + ICON_CHIP, + ICON_SIGN_DIRECTION, +) + +from . import CONF_LD2450_ID, LD2450Component + +DEPENDENCIES = ["ld2450"] + +MAX_TARGETS = 3 + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_CHIP, + ), + cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_BLUETOOTH, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"target_{n + 1}"): cv.Schema( + { + cv.Optional(CONF_DIRECTION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_NONE, + icon=ICON_SIGN_DIRECTION, + ), + } + ) + for n in range(MAX_TARGETS) + } +) + + +async def to_code(config): + ld2450_component = await cg.get_variable(config[CONF_LD2450_ID]) + if version_config := config.get(CONF_VERSION): + sens = await text_sensor.new_text_sensor(version_config) + cg.add(ld2450_component.set_version_text_sensor(sens)) + if mac_address_config := config.get(CONF_MAC_ADDRESS): + sens = await text_sensor.new_text_sensor(mac_address_config) + cg.add(ld2450_component.set_mac_text_sensor(sens)) + for n in range(MAX_TARGETS): + if direction_conf := config.get(f"target_{n + 1}"): + if direction_config := direction_conf.get(CONF_DIRECTION): + sens = await text_sensor.new_text_sensor(direction_config) + cg.add(ld2450_component.set_direction_text_sensor(n, sens)) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index e1002478a1..99f8ad76d8 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -36,6 +36,7 @@ from esphome.const import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_PORT, + CONF_PUBLISH_NAN_AS_NONE, CONF_QOS, CONF_REBOOT_TIMEOUT, CONF_RETAIN, @@ -49,7 +50,6 @@ from esphome.const import ( CONF_USE_ABBREVIATIONS, CONF_USERNAME, CONF_WILL_MESSAGE, - CONF_PUBLISH_NAN_AS_NONE, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -406,7 +406,7 @@ async def to_code(config): if CONF_SSL_FINGERPRINTS in config: for fingerprint in config[CONF_SSL_FINGERPRINTS]: arr = [ - cg.RawExpression(f"0x{fingerprint[i:i + 2]}") for i in range(0, 40, 2) + cg.RawExpression(f"0x{fingerprint[i : i + 2]}") for i in range(0, 40, 2) ] cg.add(var.add_ssl_fingerprint(arr)) cg.add_build_flag("-DASYNC_TCP_SSL_ENABLED=1") diff --git a/esphome/components/nfc/binary_sensor/__init__.py b/esphome/components/nfc/binary_sensor/__init__.py index 21c8298ea8..47cf014550 100644 --- a/esphome/components/nfc/binary_sensor/__init__.py +++ b/esphome/components/nfc/binary_sensor/__init__.py @@ -1,9 +1,10 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import binary_sensor +import esphome.config_validation as cv from esphome.const import CONF_UID from esphome.core import HexInt -from .. import nfc_ns, Nfcc, NfcTagListener + +from .. import Nfcc, NfcTagListener, nfc_ns DEPENDENCIES = ["nfc"] @@ -25,8 +26,7 @@ def validate_uid(value): for x in value.split("-"): if len(x) != 2: raise cv.Invalid( - "Each part (separated by '-') of the UID must be two characters " - "long." + "Each part (separated by '-') of the UID must be two characters long." ) try: x = int(x, 16) diff --git a/esphome/components/opentherm/binary_sensor/__init__.py b/esphome/components/opentherm/binary_sensor/__init__.py index 643734f90c..d4c7861a1d 100644 --- a/esphome/components/opentherm/binary_sensor/__init__.py +++ b/esphome/components/opentherm/binary_sensor/__init__.py @@ -1,8 +1,9 @@ from typing import Any -import esphome.config_validation as cv from esphome.components import binary_sensor -from .. import const, schema, validate, generate +import esphome.config_validation as cv + +from .. import const, generate, schema, validate DEPENDENCIES = [const.OPENTHERM] COMPONENT_TYPE = const.BINARY_SENSOR @@ -11,8 +12,7 @@ COMPONENT_TYPE = const.BINARY_SENSOR def get_entity_validation_schema(entity: schema.BinarySensorSchema) -> cv.Schema: return binary_sensor.binary_sensor_schema( device_class=( - entity.device_class - or binary_sensor._UNDEF # pylint: disable=protected-access + entity.device_class or binary_sensor._UNDEF # pylint: disable=protected-access ), icon=(entity.icon or binary_sensor._UNDEF), # pylint: disable=protected-access ) diff --git a/esphome/components/opentherm/generate.py b/esphome/components/opentherm/generate.py index 6b6a0255a8..a97754d52c 100644 --- a/esphome/components/opentherm/generate.py +++ b/esphome/components/opentherm/generate.py @@ -3,8 +3,9 @@ from typing import Any, Callable, Optional import esphome.codegen as cg from esphome.const import CONF_ID + from . import const -from .schema import TSchema, SettingSchema +from .schema import SettingSchema, TSchema opentherm_ns = cg.esphome_ns.namespace("opentherm") OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component) @@ -112,11 +113,10 @@ def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}") if keep_updated: cg.add(hub.add_repeating_message(msg_expr)) + elif order is not None: + cg.add(hub.add_initial_message(msg_expr, order)) else: - if order is not None: - cg.add(hub.add_initial_message(msg_expr, order)) - else: - cg.add(hub.add_initial_message(msg_expr)) + cg.add(hub.add_initial_message(msg_expr)) def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None: @@ -128,7 +128,7 @@ Create = Callable[[dict[str, Any], str, cg.MockObj], Awaitable[cg.Pvariable]] def create_only_conf( - create: Callable[[dict[str, Any]], Awaitable[cg.Pvariable]] + create: Callable[[dict[str, Any]], Awaitable[cg.Pvariable]], ) -> Create: return lambda conf, _key, _hub: create(conf) diff --git a/esphome/components/opentherm/sensor/__init__.py b/esphome/components/opentherm/sensor/__init__.py index 546a79054b..86c842b299 100644 --- a/esphome/components/opentherm/sensor/__init__.py +++ b/esphome/components/opentherm/sensor/__init__.py @@ -1,8 +1,9 @@ from typing import Any -import esphome.config_validation as cv from esphome.components import sensor -from .. import const, schema, validate, generate +import esphome.config_validation as cv + +from .. import const, generate, schema, validate DEPENDENCIES = [const.OPENTHERM] COMPONENT_TYPE = const.SENSOR @@ -22,11 +23,9 @@ MSG_DATA_TYPES = { def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema: return sensor.sensor_schema( - unit_of_measurement=entity.unit_of_measurement - or sensor._UNDEF, # pylint: disable=protected-access + unit_of_measurement=entity.unit_of_measurement or sensor._UNDEF, # pylint: disable=protected-access accuracy_decimals=entity.accuracy_decimals, - device_class=entity.device_class - or sensor._UNDEF, # pylint: disable=protected-access + device_class=entity.device_class or sensor._UNDEF, # pylint: disable=protected-access icon=entity.icon or sensor._UNDEF, # pylint: disable=protected-access state_class=entity.state_class, ).extend( diff --git a/esphome/components/pn532/binary_sensor.py b/esphome/components/pn532/binary_sensor.py index 9bcae30750..b9c3103c65 100644 --- a/esphome/components/pn532/binary_sensor.py +++ b/esphome/components/pn532/binary_sensor.py @@ -1,9 +1,10 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import binary_sensor +import esphome.config_validation as cv from esphome.const import CONF_UID from esphome.core import HexInt -from . import pn532_ns, PN532, CONF_PN532_ID + +from . import CONF_PN532_ID, PN532, pn532_ns DEPENDENCIES = ["pn532"] @@ -13,8 +14,7 @@ def validate_uid(value): for x in value.split("-"): if len(x) != 2: raise cv.Invalid( - "Each part (separated by '-') of the UID must be two characters " - "long." + "Each part (separated by '-') of the UID must be two characters long." ) try: x = int(x, 16) diff --git a/esphome/components/rc522/binary_sensor.py b/esphome/components/rc522/binary_sensor.py index 716c0eca76..87f81c2223 100644 --- a/esphome/components/rc522/binary_sensor.py +++ b/esphome/components/rc522/binary_sensor.py @@ -1,9 +1,10 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import binary_sensor +import esphome.config_validation as cv from esphome.const import CONF_UID from esphome.core import HexInt -from . import rc522_ns, RC522, CONF_RC522_ID + +from . import CONF_RC522_ID, RC522, rc522_ns DEPENDENCIES = ["rc522"] @@ -13,8 +14,7 @@ def validate_uid(value): for x in value.split("-"): if len(x) != 2: raise cv.Invalid( - "Each part (separated by '-') of the UID must be two characters " - "long." + "Each part (separated by '-') of the UID must be two characters long." ) try: x = int(x, 16) diff --git a/esphome/components/remote_base/toto_protocol.h b/esphome/components/remote_base/toto_protocol.h index e62714bbbf..6a635b0f7c 100644 --- a/esphome/components/remote_base/toto_protocol.h +++ b/esphome/components/remote_base/toto_protocol.h @@ -36,7 +36,7 @@ template class TotoAction : public RemoteTransmitterActionBaserc_code_2_.value(x...); data.command = this->command_.value(x...); this->set_send_times(this->send_times_.value_or(x..., 3)); - this->set_send_wait(this->send_wait_.value_or(x..., 32000)); + this->set_send_wait(this->send_wait_.value_or(x..., 36000)); TotoProtocol().encode(dst, data); } }; diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index a529bbd474..638aad7c06 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -137,7 +137,7 @@ def validate_temperature_preset(preset, root_config, name, requirements): def generate_comparable_preset(config, name): - comparable_preset = f"{CONF_PRESET}:\n" f" - {CONF_NAME}: {name}\n" + comparable_preset = f"{CONF_PRESET}:\n - {CONF_NAME}: {name}\n" if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: comparable_preset += f" {CONF_DEFAULT_TARGET_TEMPERATURE_LOW}: {config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]}\n" diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 27d11e4ded..9f7ce4d2e3 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1223,8 +1223,7 @@ def subscribe_topic(value): if index != len(value) - 1: # If there are multiple wildcards, this will also trigger raise Invalid( - "Multi-level wildcard must be the last " - "character in the topic filter." + "Multi-level wildcard must be the last character in the topic filter." ) if len(value) > 1 and value[index - 1] != "/": raise Invalid("Multi-level wildcard must be after a topic level separator.") diff --git a/esphome/core/config.py b/esphome/core/config.py index 2077af02a7..359b78acf1 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -1,5 +1,4 @@ import logging -import multiprocessing import os from pathlib import Path @@ -94,10 +93,19 @@ def valid_project_name(value: str): return value +def get_usable_cpu_count() -> int: + """Return the number of CPUs that can be used for processes. + On Python 3.13+ this is the number of CPUs that can be used for processes. + On older Python versions this is the number of CPUs. + """ + return ( + os.process_cpu_count() if hasattr(os, "process_cpu_count") else os.cpu_count() + ) + + if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ: _compile_process_limit_default = min( - int(os.environ["ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT"]), - multiprocessing.cpu_count(), + int(os.environ["ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT"]), get_usable_cpu_count() ) else: _compile_process_limit_default = cv.UNDEFINED @@ -156,7 +164,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional( CONF_COMPILE_PROCESS_LIMIT, default=_compile_process_limit_default - ): cv.int_range(min=1, max=multiprocessing.cpu_count()), + ): cv.int_range(min=1, max=get_usable_cpu_count()), } ), validate_hostname, diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 4e283868e1..eb0bd25d1d 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -506,9 +506,9 @@ def with_local_variable(id_: ID, rhs: SafeExpType, callback: Callable, *args) -> """ # throw if the callback is async: - assert not inspect.iscoroutinefunction( - callback - ), "with_local_variable() callback cannot be async!" + assert not inspect.iscoroutinefunction(callback), ( + "with_local_variable() callback cannot be async!" + ) CORE.add(RawStatement("{")) # output opening curly brace obj = variable(id_, rhs, None, True) diff --git a/esphome/wizard.py b/esphome/wizard.py index eecbbdb172..7fdf245c76 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -144,17 +144,17 @@ def wizard_file(**kwargs): # Configure API if "password" in kwargs: - config += f" password: \"{kwargs['password']}\"\n" + config += f' password: "{kwargs["password"]}"\n' if "api_encryption_key" in kwargs: - config += f" encryption:\n key: \"{kwargs['api_encryption_key']}\"\n" + config += f' encryption:\n key: "{kwargs["api_encryption_key"]}"\n' # Configure OTA config += "\nota:\n" config += " - platform: esphome\n" if "ota_password" in kwargs: - config += f" password: \"{kwargs['ota_password']}\"" + config += f' password: "{kwargs["ota_password"]}"' elif "password" in kwargs: - config += f" password: \"{kwargs['password']}\"" + config += f' password: "{kwargs["password"]}"' # Configuring wifi config += "\n\nwifi:\n" @@ -181,18 +181,14 @@ def wizard_file(**kwargs): password: "{fallback_psk}" captive_portal: - """.format( - **kwargs - ) + """.format(**kwargs) else: config += """ # Enable fallback hotspot in case wifi connection fails ap: ssid: "{fallback_name}" password: "{fallback_psk}" - """.format( - **kwargs - ) + """.format(**kwargs) return config @@ -388,19 +384,19 @@ def wizard(path): safe_print() # Don't sleep because user needs to copy link if platform == "ESP32": - safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcu-32s')}\".") + safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcu-32s")}".') boards_list = esp32_boards.BOARDS.items() elif platform == "ESP8266": - safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcuv2')}\".") + safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcuv2")}".') boards_list = esp8266_boards.BOARDS.items() elif platform == "BK72XX": - safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'cb2s')}\".") + safe_print(f'For example "{color(Fore.BOLD_WHITE, "cb2s")}".') boards_list = bk72xx_boards.BOARDS.items() elif platform == "RTL87XX": - safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'wr3')}\".") + safe_print(f'For example "{color(Fore.BOLD_WHITE, "wr3")}".') boards_list = rtl87xx_boards.BOARDS.items() elif platform == "RP2040": - safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'rpipicow')}\".") + safe_print(f'For example "{color(Fore.BOLD_WHITE, "rpipicow")}".') boards_list = rp2040_boards.BOARDS.items() else: @@ -439,7 +435,7 @@ def wizard(path): f"First, what's the {color(Fore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?" ) sleep(1.5) - safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'Abraham Linksys')}\".") + safe_print(f'For example "{color(Fore.BOLD_WHITE, "Abraham Linksys")}".') while True: ssid = safe_input(color(Fore.BOLD_WHITE, "(ssid): ")) try: @@ -465,7 +461,7 @@ def wizard(path): f"Now please state the {color(Fore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)" ) safe_print() - safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'PASSWORD42')}\"") + safe_print(f'For example "{color(Fore.BOLD_WHITE, "PASSWORD42")}"') sleep(0.5) psk = safe_input(color(Fore.BOLD_WHITE, "(PSK): ")) safe_print( diff --git a/esphome/writer.py b/esphome/writer.py index 90446ae4b1..39423db64c 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -212,9 +212,7 @@ def write_platformio_project(): write_platformio_ini(content) -DEFINES_H_FORMAT = ( - ESPHOME_H_FORMAT -) = """\ +DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ #pragma once #include "esphome/core/macros.h" {} diff --git a/requirements.txt b/requirements.txt index c15dcbbbf7..1de6e3dd06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ esptool==4.7.0 click==8.1.7 esphome-dashboard==20250212.0 aioesphomeapi==29.1.0 -zeroconf==0.144.3 +zeroconf==0.145.1 puremagic==1.27 ruamel.yaml==0.18.6 # dashboard_import esphome-glyphsets==0.1.0 diff --git a/requirements_test.txt b/requirements_test.txt index 5d94f7f640..d836efc148 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.2.7 flake8==7.0.0 # also change in .pre-commit-config.yaml when updating -black==24.4.2 # also change in .pre-commit-config.yaml when updating +ruff==0.9.2 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.2 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/script/clang-format b/script/clang-format index d922c5b6f1..b1e84a56b7 100755 --- a/script/clang-format +++ b/script/clang-format @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import argparse -import multiprocessing import os import queue import re @@ -11,7 +10,13 @@ import threading import click import colorama -from helpers import filter_changed, get_binary, git_ls_files, print_error_for_file +from helpers import ( + filter_changed, + get_binary, + get_usable_cpu_count, + git_ls_files, + print_error_for_file, +) def run_format(executable, args, queue, lock, failed_files): @@ -25,7 +30,9 @@ def run_format(executable, args, queue, lock, failed_files): invocation.extend(["--dry-run", "-Werror"]) invocation.append(path) - proc = subprocess.run(invocation, capture_output=True, encoding="utf-8") + proc = subprocess.run( + invocation, capture_output=True, encoding="utf-8", check=False + ) if proc.returncode != 0: with lock: print_error_for_file(path, proc.stderr) @@ -45,7 +52,7 @@ def main(): "-j", "--jobs", type=int, - default=multiprocessing.cpu_count(), + default=get_usable_cpu_count(), help="number of format instances to be run in parallel.", ) parser.add_argument( @@ -80,7 +87,8 @@ def main(): lock = threading.Lock() for _ in range(args.jobs): t = threading.Thread( - target=run_format, args=(executable, args, task_queue, lock, failed_files) + target=run_format, + args=(executable, args, task_queue, lock, failed_files), ) t.daemon = True t.start() @@ -95,7 +103,7 @@ def main(): # Wait for all threads to be done. task_queue.join() - except FileNotFoundError as ex: + except FileNotFoundError: return 1 except KeyboardInterrupt: print() @@ -103,7 +111,7 @@ def main(): # Kill subprocesses (and ourselves!) # No simple, clean alternative appears to be available. os.kill(0, 9) - return 2 # Will not execute. + return 2 # Will not execute. return len(failed_files) diff --git a/script/clang-tidy b/script/clang-tidy index 5c19f81043..51705f955b 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import argparse -import multiprocessing import os import queue import re @@ -19,6 +18,7 @@ from helpers import ( filter_changed, filter_grep, get_binary, + get_usable_cpu_count, git_ls_files, load_idedata, print_error_for_file, @@ -170,7 +170,7 @@ def main(): "-j", "--jobs", type=int, - default=multiprocessing.cpu_count(), + default=get_usable_cpu_count(), help="number of tidy instances to be run in parallel.", ) parser.add_argument( diff --git a/script/helpers.py b/script/helpers.py index 6f36faaeb1..6148371e32 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -188,3 +188,14 @@ def get_binary(name: str, version: str) -> str: """ ) raise + + +def get_usable_cpu_count() -> int: + """Return the number of CPUs that can be used for processes. + + On Python 3.13+ this is the number of CPUs that can be used for processes. + On older Python versions this is the number of CPUs. + """ + return ( + os.process_cpu_count() if hasattr(os, "process_cpu_count") else os.cpu_count() + ) diff --git a/tests/components/ld2450/common.yaml b/tests/components/ld2450/common.yaml new file mode 100644 index 0000000000..2e62efb0f5 --- /dev/null +++ b/tests/components/ld2450/common.yaml @@ -0,0 +1,168 @@ +uart: + - id: ld2450_uart + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 256000 + parity: NONE + stop_bits: 1 + +ld2450: + - id: ld2450_radar + uart_id: ld2450_uart + throttle: 1000ms + +button: + - platform: ld2450 + ld2450_id: ld2450_radar + factory_reset: + name: LD2450 Factory Reset + entity_category: config + restart: + name: LD2450 Restart + entity_category: config + +sensor: + - platform: ld2450 + ld2450_id: ld2450_radar + target_count: + name: Presence Target Count + still_target_count: + name: Still Target Count + moving_target_count: + name: Moving Target Count + target_1: + x: + name: Target-1 X + y: + name: Target-1 Y + speed: + name: Target-1 Speed + angle: + name: Target-1 Angle + distance: + name: Target-1 Distance + resolution: + name: Target-1 Resolution + target_2: + x: + name: Target-2 X + y: + name: Target-2 Y + speed: + name: Target-2 Speed + angle: + name: Target-2 Angle + distance: + name: Target-2 Distance + resolution: + name: Target-2 Resolution + target_3: + x: + name: Target-3 X + y: + name: Target-3 Y + speed: + name: Target-3 Speed + angle: + name: Target-3 Angle + distance: + name: Target-3 Distance + resolution: + name: Target-3 Resolution + zone_1: + target_count: + name: Zone-1 All Target Count + still_target_count: + name: Zone-1 Still Target Count + moving_target_count: + name: Zone-1 Moving Target Count + zone_2: + target_count: + name: Zone-2 All Target Count + still_target_count: + name: Zone-2 Still Target Count + moving_target_count: + name: Zone-2 Moving Target Count + zone_3: + target_count: + name: Zone-3 All Target Count + still_target_count: + name: Zone-3 Still Target Count + moving_target_count: + name: Zone-3 Moving Target Count + +binary_sensor: + - platform: ld2450 + ld2450_id: ld2450_radar + has_target: + name: Presence + has_moving_target: + name: Moving Target + has_still_target: + name: Still Target + +switch: + - platform: ld2450 + ld2450_id: ld2450_radar + bluetooth: + name: Bluetooth + multi_target: + name: Multi Target Tracking + +text_sensor: + - platform: ld2450 + ld2450_id: ld2450_radar + version: + name: LD2450 Firmware + mac_address: + name: LD2450 BT MAC + target_1: + direction: + name: Target-1 Direction + target_2: + direction: + name: Target-2 Direction + target_3: + direction: + name: Target-3 Direction + +number: + - platform: ld2450 + ld2450_id: ld2450_radar + presence_timeout: + name: Timeout + zone_1: + x1: + name: Zone-1 X1 + y1: + name: Zone-1 Y1 + x2: + name: Zone-1 X2 + y2: + name: Zone-1 Y2 + zone_2: + x1: + name: Zone-2 X1 + y1: + name: Zone-2 Y1 + x2: + name: Zone-2 X2 + y2: + name: Zone-2 Y2 + zone_3: + x1: + name: Zone-3 X1 + y1: + name: Zone-3 Y1 + x2: + name: Zone-3 X2 + y2: + name: Zone-3 Y2 + +select: + - platform: ld2450 + ld2450_id: ld2450_radar + baud_rate: + name: Baud Rate + zone_type: + name: Zone Type diff --git a/tests/components/ld2450/test.esp32-ard.yaml b/tests/components/ld2450/test.esp32-ard.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/ld2450/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ld2450/test.esp32-c3-ard.yaml b/tests/components/ld2450/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2450/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2450/test.esp32-c3-idf.yaml b/tests/components/ld2450/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2450/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2450/test.esp32-idf.yaml b/tests/components/ld2450/test.esp32-idf.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/ld2450/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ld2450/test.esp8266-ard.yaml b/tests/components/ld2450/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2450/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2450/test.rp2040-ard.yaml b/tests/components/ld2450/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2450/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 6f4b5a40bc..95633ca0c6 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -1,11 +1,9 @@ from collections.abc import Iterator - import math import pytest -from esphome import cpp_generator as cg -from esphome import cpp_types as ct +from esphome import cpp_generator as cg, cpp_types as ct class TestExpressions: @@ -156,10 +154,7 @@ class TestLambdaExpression: actual = str(target) assert actual == ( - "[=](int32_t foo, float bar) {\n" - " if ((foo == 5) && (bar < 10))) {\n" - " }\n" - "}" + "[=](int32_t foo, float bar) {\n if ((foo == 5) && (bar < 10))) {\n }\n}" ) def test_str__with_return(self):